From 9246385dac9f16ac7dbbf0ff4ed1344e1e3700bf Mon Sep 17 00:00:00 2001 From: Samuel Marks <807580+SamuelMarks@users.noreply.github.com> Date: Wed, 7 Feb 2024 23:20:01 -0500 Subject: [PATCH] [cdd/__main__.py] Add `--extra-module` option; [cdd/compound/{exmod,exmod_utils}.py] Implement `--extra-module` functionality and automatically add SQLalchemy mod if generated by `exmod` ; [README.md] Update `--help` text to match current cmd --- README.md | 33 +++-- cdd/__init__.py | 2 +- cdd/__main__.py | 12 +- cdd/argparse_function/emit.py | 29 ++-- cdd/class_/emit.py | 8 +- cdd/class_/parse.py | 31 +++-- cdd/compound/exmod.py | 76 +++++++--- cdd/compound/exmod_utils.py | 34 ++++- cdd/compound/openapi/utils/emit_utils.py | 4 +- cdd/docstring/utils/emit_utils.py | 4 +- cdd/function/emit.py | 20 +-- cdd/function/parse.py | 8 +- cdd/function/utils/emit_utils.py | 16 ++- cdd/function/utils/parse_utils.py | 18 ++- cdd/json_schema/utils/emit_utils.py | 19 ++- cdd/json_schema/utils/parse_utils.py | 4 +- cdd/shared/ast_utils.py | 84 ++++++++++- cdd/shared/docstring_parsers.py | 22 +-- cdd/shared/emit/file.py | 4 +- cdd/shared/parse/utils/parser_utils.py | 4 +- cdd/shared/source_transformer.py | 4 +- cdd/sqlalchemy/utils/emit_utils.py | 147 +++++++++++++------- cdd/sqlalchemy/utils/parse_utils.py | 34 +++-- cdd/sqlalchemy/utils/shared_utils.py | 30 ++-- cdd/tests/test_compound/test_exmod.py | 21 ++- cdd/tests/test_compound/test_exmod_utils.py | 21 +-- cdd/tests/test_shared/test_ast_utils.py | 86 ++++++++++++ cdd/tests/utils_for_tests.py | 2 +- 28 files changed, 564 insertions(+), 213 deletions(-) diff --git a/README.md b/README.md index cab80dde..21dd2ad1 100644 --- a/README.md +++ b/README.md @@ -482,9 +482,9 @@ class Config(Base): $ python -m cdd --help usage: python -m cdd [-h] [--version] - {sync_properties,sync,gen,gen_routes,openapi,doctrans,exmod} - ... - + {sync_properties,sync,gen,gen_routes,openapi,doctrans,exmod} + ... + Open API to/fro routes, models, and tests. Convert between docstrings, classes, methods, argparse, pydantic, and SQLalchemy. @@ -575,7 +575,7 @@ class Config(Base): [--parse {argparse,class,function,json_schema,pydantic,sqlalchemy,sqlalchemy_hybrid,sqlalchemy_table,infer}] --emit {argparse,class,function,json_schema,pydantic,sqlalchemy,sqlalchemy_hybrid,sqlalchemy_table} - --output-filename OUTPUT_FILENAME [--emit-call] + -o OUTPUT_FILENAME [--emit-call] [--emit-and-infer-imports] [--no-word-wrap] [--decorator DECORATOR_LIST] [--phase PHASE] @@ -593,7 +593,7 @@ class Config(Base): What type the input is. --emit {argparse,class,function,json_schema,pydantic,sqlalchemy,sqlalchemy_hybrid,sqlalchemy_table} Which type to generate. - --output-filename OUTPUT_FILENAME, -o OUTPUT_FILENAME + -o OUTPUT_FILENAME, --output-filename OUTPUT_FILENAME Output file to write to. --emit-call Whether to place all the previous body into a new `__call__` internal function @@ -678,20 +678,27 @@ PS: If you're outputting JSON-schema and want a file per schema then: ### `exmod` $ python -m cdd exmod --help - usage: python -m cdd exmod [-h] --module MODULE --emit + usage: python -m cdd exmod [-h] -m MODULE --emit {argparse,class,function,json_schema,pydantic,sqlalchemy,sqlalchemy_hybrid,sqlalchemy_table} - [--no-word-wrap] [--blacklist BLACKLIST] - [--whitelist WHITELIST] --output-directory + [--emit-sqlalchemy-submodule] + [--extra-module [EXTRA_MODULES]] [--no-word-wrap] + [--blacklist BLACKLIST] [--whitelist WHITELIST] -o OUTPUT_DIRECTORY - [--target-module-name TARGET_MODULE_NAME] + [--target-module-name TARGET_MODULE_NAME] [-r] [--dry-run] options: -h, --help show this help message and exit - --module MODULE, -m MODULE + -m MODULE, --module MODULE The module or fully-qualified name (FQN) to expose. --emit {argparse,class,function,json_schema,pydantic,sqlalchemy,sqlalchemy_hybrid,sqlalchemy_table} Which type to generate. + --emit-sqlalchemy-submodule + Whether to; for sqlalchemy*; emit submodule "sqlalchem + y_mod/{__init__,connection,create_tables}.py" + --extra-module [EXTRA_MODULES] + Additional module(s) to expose; specifiable multiple + times. Added to symbol auto-import resolver. --no-word-wrap Whether word-wrap is disabled (on emission). None enables word-wrap. Defaults to None. --blacklist BLACKLIST @@ -700,11 +707,13 @@ PS: If you're outputting JSON-schema and want a file per schema then: --whitelist WHITELIST Modules/FQN to emit. If unspecified will emit all (minus blacklist). - --output-directory OUTPUT_DIRECTORY, -o OUTPUT_DIRECTORY + -o OUTPUT_DIRECTORY, --output-directory OUTPUT_DIRECTORY Where to place the generated exposed interfaces to the given `--module`. --target-module-name TARGET_MODULE_NAME - Target module name. Defaults to `${module}.gold`. + Target module name. Defaults to `${module}___gold`. + -r, --recursive Recursively traverse module hierarchy and recreate + hierarchy with exposed interfaces --dry-run Show what would be created; don't actually write to the filesystem. diff --git a/cdd/__init__.py b/cdd/__init__.py index 0142c383..b43113ae 100644 --- a/cdd/__init__.py +++ b/cdd/__init__.py @@ -9,7 +9,7 @@ from logging import getLogger as get_logger __author__ = "Samuel Marks" # type: str -__version__ = "0.0.99rc24" # type: str +__version__ = "0.0.99rc25" # type: str __description__ = ( "Open API to/fro routes, models, and tests. " "Convert between docstrings, classes, methods, argparse, pydantic, and SQLalchemy." diff --git a/cdd/__main__.py b/cdd/__main__.py index 4a980207..d0ec1974 100644 --- a/cdd/__main__.py +++ b/cdd/__main__.py @@ -401,11 +401,17 @@ def _build_parser(): action="append", ) exmod_parser.add_argument( - "--emit-base-engine-metadata", - help="[sqlalchemy specific] Whether to produce a file with `Base`, `engine`, and `metadata`." - "`None` is `False`. Defaults to None.", + "--emit-sqlalchemy-submodule", + help='Whether to; for sqlalchemy*; emit submodule "sqlalchemy_mod/{__init__,connection,create_tables}.py"', action="store_true", ) + exmod_parser.add_argument( + "--extra-module", + help="Additional module(s) to expose; specifiable multiple times. Added to symbol auto-import resolver.", + action="append", + nargs="?", + dest="extra_modules", + ) exmod_parser.add_argument( "--no-word-wrap", help="Whether word-wrap is disabled (on emission). None enables word-wrap. Defaults to None.", diff --git a/cdd/argparse_function/emit.py b/cdd/argparse_function/emit.py index 0dac6a27..861e9489 100644 --- a/cdd/argparse_function/emit.py +++ b/cdd/argparse_function/emit.py @@ -20,14 +20,8 @@ from itertools import chain from typing import Optional +import cdd.shared.ast_utils from cdd.docstring.emit import docstring -from cdd.shared.ast_utils import ( - get_value, - maybe_type_comment, - param2argparse_param, - set_arg, - set_value, -) from cdd.shared.emit.utils.emitter_utils import get_internal_body from cdd.shared.pure_utils import code_quoted, fill, identity, none_types from cdd.shared.types import Internal @@ -87,7 +81,7 @@ def argparse_function( return FunctionDef( args=arguments( - args=[set_arg("argument_parser")], + args=[cdd.shared.ast_utils.set_arg("argument_parser")], # None if function_type in frozenset((None, "static")) # else set_arg(function_type), defaults=[], @@ -104,7 +98,7 @@ def argparse_function( iter( ( Expr( - set_value( + cdd.shared.ast_utils.set_value( docstring( { "doc": "Set CLI arguments", @@ -200,14 +194,14 @@ def argparse_function( col_offset=None, ) ], - value=set_value( + value=cdd.shared.ast_utils.set_value( (fill if wrap_description else identity)( intermediate_repr["doc"] ) ), lineno=None, expr=None, - **maybe_type_comment + **cdd.shared.ast_utils.maybe_type_comment ), ) ), @@ -218,7 +212,7 @@ def argparse_function( ( map( partial( - param2argparse_param, + cdd.shared.ast_utils.param2argparse_param, word_wrap=word_wrap, emit_default_doc=emit_default_doc, ), @@ -241,7 +235,12 @@ def argparse_function( ] if internal_body and isinstance(internal_body[0], Expr) - and isinstance(get_value(internal_body[0].value), str) + and isinstance( + cdd.shared.ast_utils.get_value( + internal_body[0].value + ), + str, + ) else internal_body ), ( @@ -260,7 +259,7 @@ def argparse_function( col_offset=None, ), ( - set_value( + cdd.shared.ast_utils.set_value( intermediate_repr["returns"][ "return_type" ]["default"] @@ -314,7 +313,7 @@ def argparse_function( arguments_args=None, identifier_name=None, stmt=None, - **maybe_type_comment + **cdd.shared.ast_utils.maybe_type_comment ) diff --git a/cdd/class_/emit.py b/cdd/class_/emit.py index 14c5668b..2e811bd4 100644 --- a/cdd/class_/emit.py +++ b/cdd/class_/emit.py @@ -9,10 +9,10 @@ from itertools import chain from typing import Optional +import cdd.shared.ast_utils from cdd.class_.utils.emit_utils import RewriteName from cdd.docstring.emit import docstring from cdd.function.utils.emit_utils import _make_call_meth -from cdd.shared.ast_utils import param2ast, set_value from cdd.shared.pure_utils import PY_GTE_3_8, PY_GTE_3_9, rpartial if PY_GTE_3_9: @@ -140,7 +140,9 @@ def class_( None if ds is None else Expr( - set_value(ds), lineno=None, col_offset=None + cdd.shared.ast_utils.set_value(ds), + lineno=None, + col_offset=None, ) ) )( @@ -157,7 +159,7 @@ def class_( ), ), map( - param2ast, + cdd.shared.ast_utils.param2ast, (intermediate_repr.get("params") or OrderedDict()).items(), ), iter( diff --git a/cdd/class_/parse.py b/cdd/class_/parse.py index 35bb0e9a..2c78dfaf 100644 --- a/cdd/class_/parse.py +++ b/cdd/class_/parse.py @@ -22,12 +22,12 @@ import cdd.docstring.parse import cdd.function.parse +import cdd.shared.ast_utils import cdd.shared.docstring_parsers import cdd.shared.parse.utils.parser_utils +import cdd.shared.source_transformer from cdd.class_.utils.parse_utils import get_source -from cdd.shared.ast_utils import NoneStr, find_ast_type, get_value, parse_to_scalar from cdd.shared.pure_utils import rpartial, simple_types -from cdd.shared.source_transformer import to_code from cdd.shared.types import IntermediateRepr @@ -89,7 +89,9 @@ def class_( ), "Expected 'Union[Module, ClassDef]' got `{node_name!r}`".format( node_name=type(class_def).__name__ ) - class_def: ClassDef = cast(ClassDef, find_ast_type(class_def, class_name)) + class_def: ClassDef = cast( + ClassDef, cdd.shared.ast_utils.find_ast_type(class_def, class_name) + ) doc_str: Optional[str] = get_docstring(class_def, clean=parse_original_whitespace) intermediate_repr: IntermediateRepr = ( { @@ -115,11 +117,11 @@ def class_( body: ClassDef.body = class_def.body if doc_str is None else class_def.body[1:] for e in body: if isinstance(e, AnnAssign): - typ: str = to_code(e.annotation).rstrip("\n") + typ: str = cdd.shared.source_transformer.to_code(e.annotation).rstrip("\n") val = ( ( lambda v: ( - {"default": NoneStr} + {"default": cdd.shared.ast_utils.NoneStr} if v is None else { "default": ( @@ -130,12 +132,15 @@ def class_( "{}": {} if isinstance(v, Dict) else set(), "[]": [], "()": (), - }.get(value, parse_to_scalar(value)) - )(to_code(v).rstrip("\n")) + }.get( + value, + cdd.shared.ast_utils.parse_to_scalar(value), + ) + )(cdd.shared.source_transformer.to_code(v).rstrip("\n")) ) } ) - )(get_value(get_value(e))) + )(cdd.shared.ast_utils.get_value(cdd.shared.ast_utils.get_value(e))) if hasattr(e, "value") and e.value is not None else {} ) @@ -159,10 +164,10 @@ def class_( intermediate_repr[k] = OrderedDict() intermediate_repr[k][target_id] = typ_default elif isinstance(e, Assign): - val = get_value(e) + val = cdd.shared.ast_utils.get_value(e) if val is not None: - val = get_value(val) + val = cdd.shared.ast_utils.get_value(val) deque( map( lambda target: setitem( @@ -181,7 +186,11 @@ def class_( ( _target_id if isinstance(target, Name) - else get_value(get_value(target)) + else cdd.shared.ast_utils.get_value( + cdd.shared.ast_utils.get_value( + target + ) + ) ), {"default": val}, ) diff --git a/cdd/compound/exmod.py b/cdd/compound/exmod.py index b1603e13..8ae55705 100644 --- a/cdd/compound/exmod.py +++ b/cdd/compound/exmod.py @@ -20,6 +20,7 @@ construct_module_with_symbols, maybe_type_comment, merge_modules, + module_to_all, set_value, ) from cdd.shared.pure_utils import ( @@ -49,11 +50,13 @@ def exmod( output_directory, target_module_name, mock_imports, - emit_base_engine_metadata, + emit_sqlalchemy_submodule, + extra_modules, no_word_wrap, recursive, dry_run, filesystem_layout="as_input", + extra_modules_to_all=None, ): """ Expose module as `emit` types into `output_directory` @@ -80,8 +83,11 @@ def exmod( :param mock_imports: Whether to generate mock TensorFlow imports :type mock_imports: ```bool``` - :param emit_base_engine_metadata: Whether to produce a file with `Base`, `engine`, and `metadata`. - :type emit_base_engine_metadata: ```bool``` + :param emit_sqlalchemy_submodule: Whether to emit submodule "sqlalchemy_mod/{__init__,connection,create_tables}.py" + :type emit_sqlalchemy_submodule: ```bool``` + + :param extra_modules: Additional module(s) to expose; specifiable multiple times. Prepended to symbol auto-importer + :type extra_modules: ```Optional[List[str]]``` :param no_word_wrap: Whether word-wrap is disabled (on emission). :type no_word_wrap: ```Optional[Literal[True]]``` @@ -94,8 +100,16 @@ def exmod( :param filesystem_layout: Hierarchy of folder and file names generated. "java" is file per package per name. :type filesystem_layout: ```Literal["java", "as_input"]``` + + :param extra_modules_to_all: Internal arg. Prepended to symbol resolver. E.g., `(("ast", {"List"}),)`. + :type extra_modules_to_all: ```Optional[tuple[tuple[str, frozenset], ...]]``` """ output_directory = path.realpath(output_directory) + extra_modules_to_all = ( + cdd.shared.ast_utils.module_to_all(extra_modules) + if extra_modules is not None and extra_modules_to_all is None + else tuple() + ) # type: tuple[tuple[str, frozenset], ...] if not isinstance(emit_name, str): deque( map( @@ -106,12 +120,14 @@ def exmod( whitelist=whitelist, mock_imports=mock_imports, filesystem_layout=filesystem_layout, - emit_base_engine_metadata=emit_base_engine_metadata, + emit_sqlalchemy_submodule=emit_sqlalchemy_submodule, + extra_modules=extra_modules, no_word_wrap=no_word_wrap, output_directory=output_directory, target_module_name=target_module_name, recursive=recursive, dry_run=dry_run, + extra_modules_to_all=extra_modules_to_all, ), emit_name or iter(()), ), @@ -150,7 +166,7 @@ def exmod( if ( emit_name in frozenset(("sqlalchemy", "sqlalchemy_hybrid", "sqlalchemy_table")) - and emit_base_engine_metadata + and emit_sqlalchemy_submodule ): sqlalchemy_mod = "sqlalchemy_mod" sqlalchemy_mod_dir_join = partial(path.join, output_directory, "sqlalchemy_mod") @@ -160,25 +176,41 @@ def exmod( "a", ).close() connection_py = "connection" - with open( - sqlalchemy_mod_dir_join( - "{name}{extsep}py".format(name=connection_py, extsep=path.extsep) - ), - "wt", - ) as f: + connection_filepath = sqlalchemy_mod_dir_join( + "{name}{extsep}py".format(name=connection_py, extsep=path.extsep) + ) + with open(connection_filepath, "wt") as f: f.write(mock_engine_base_metadata_str) sqlalchemy_module_name = ".".join( - (path.basename(output_directory), sqlalchemy_mod, connection_py) + (path.basename(output_directory), sqlalchemy_mod) ) - with open( - sqlalchemy_mod_dir_join( - "create_tables{extsep}py".format(extsep=path.extsep) - ), - "wt", - ) as f: - f.write(to_code(generate_create_tables_mod(sqlalchemy_module_name))) + sqlalchemy_module_name_connection_py = ".".join( + (sqlalchemy_module_name, connection_py) + ) + sqlalchemy_module_name_create_table = ".".join( + (sqlalchemy_module_name, "create_tables") + ) + create_table_filepath = sqlalchemy_mod_dir_join( + "create_tables{extsep}py".format(extsep=path.extsep) + ) + with open(create_table_filepath, "wt") as f: + f.write( + to_code( + generate_create_tables_mod(sqlalchemy_module_name_connection_py) + ) + ) + extra_modules_to_all = ( + ( + sqlalchemy_module_name_connection_py, + frozenset(module_to_all(connection_filepath)), + ), + ( + sqlalchemy_module_name_create_table, + frozenset(module_to_all(create_table_filepath)), + ), + ) + extra_modules_to_all try: module_root_dir: str = path.dirname( find_module_filepath( @@ -199,6 +231,7 @@ def exmod( module_root=module_root, new_module_name=new_module_name, filesystem_layout=filesystem_layout, + extra_modules_to_all=extra_modules_to_all, ) packages: typing.List[str] = find_packages( module_root_dir, @@ -281,6 +314,7 @@ def exmod_single_folder( module_name, new_module_name, filesystem_layout, + extra_modules_to_all, ): """ Expose module as `emit` types into `output_directory`. Single folder (non-recursive). @@ -324,6 +358,9 @@ def exmod_single_folder( :param filesystem_layout: Hierarchy of folder and file names generated. "java" is file per package per name. :type filesystem_layout: ```Literal["java", "as_input"]``` + + :param extra_modules_to_all: Internal arg. Prepended to symbol resolver. E.g., `(("ast", {"List"}),)`. + :type extra_modules_to_all: ```Optional[tuple[tuple[str, frozenset], ...]]``` """ mod_path: str = ( module_name @@ -351,6 +388,7 @@ def exmod_single_folder( no_word_wrap=no_word_wrap, dry_run=dry_run, filesystem_layout=filesystem_layout, + extra_modules_to_all=extra_modules_to_all, ) imports = _emit_files_from_module_and_return_imports( diff --git a/cdd/compound/exmod_utils.py b/cdd/compound/exmod_utils.py index 0d1eb258..18cc1c6e 100644 --- a/cdd/compound/exmod_utils.py +++ b/cdd/compound/exmod_utils.py @@ -23,7 +23,6 @@ import cdd.shared.ast_utils import cdd.shared.emit.file import cdd.sqlalchemy.emit -from cdd.shared.ast_utils import deduplicate_sorted_imports, infer_imports from cdd.shared.parse.utils.parser_utils import get_parser from cdd.shared.pkg_utils import relative_filename from cdd.shared.pure_utils import ( @@ -202,6 +201,7 @@ def emit_file_on_hierarchy( new_module_name, mock_imports, filesystem_layout, + extra_modules_to_all, output_directory, no_word_wrap, dry_run, @@ -228,6 +228,9 @@ def emit_file_on_hierarchy( :param filesystem_layout: Hierarchy of folder and file names generated. "java" is file per package per name. :type filesystem_layout: ```Literal["java", "as_input"]``` + :param extra_modules_to_all: Internal arg. Prepended to symbol resolver. E.g., `(("ast", {"List"}),)`. + :type extra_modules_to_all: ```Optional[tuple[tuple[str, frozenset], ...]]``` + :param output_directory: Where to place the generated exposed interfaces to the given `--module`. :type output_directory: ```str``` @@ -353,6 +356,7 @@ def emit_file_on_hierarchy( isfile_emit_filename, name, mock_imports, + extra_modules_to_all, no_word_wrap, dry_run, ) @@ -387,6 +391,7 @@ def _emit_symbol( isfile_emit_filename, name, mock_imports, + extra_modules_to_all, no_word_wrap, dry_run, ): @@ -432,6 +437,9 @@ def _emit_symbol( :param mock_imports: Whether to generate mock TensorFlow imports :type mock_imports: ```bool``` + :param extra_modules_to_all: Additional module(s) to expose. Prepended to symbol resolver. `(("ast", {"List"}),)`. + :type extra_modules_to_all: ```Optional[tuple[tuple[str, frozenset], ...]]``` + :param no_word_wrap: Whether word-wrap is disabled (on emission). :type no_word_wrap: ```Optional[Literal[True]]``` @@ -474,6 +482,12 @@ def _emit_symbol( **{"function_type": "static"} if emit_name == "function" else {} ) ) + modules_to_all = (extra_modules_to_all or tuple()) + ( + cdd.shared.ast_utils.DEFAULT_MODULES_TO_ALL_SQL_FIRST + if emit_name + in frozenset(("sqlalchemy", "sqlalchemy_hybrid", "sqlalchemy_table")) + else cdd.shared.ast_utils.DEFAULT_MODULES_TO_ALL + ) __all___node: Assign = Assign( targets=[Name("__all__", Store(), lineno=None, col_offset=None)], value=List( @@ -505,7 +519,12 @@ def _emit_symbol( ( imports_header_ast if mock_imports - else (infer_imports(gen_node) or iter(())) + else ( + cdd.shared.ast_utils.infer_imports( + gen_node, modules_to_all=modules_to_all + ) + or iter(()) + ) ), (gen_node, __all___node), ) @@ -519,14 +538,16 @@ def _emit_symbol( gen_node: Module = cdd.shared.ast_utils.merge_modules( cast(Module, existent_mod), gen_node ) - inferred_imports = infer_imports(gen_node) + inferred_imports = cdd.shared.ast_utils.infer_imports( + gen_node, modules_to_all=modules_to_all + ) if inferred_imports: gen_node.body = list( chain.from_iterable( ((gen_node.body[0],), inferred_imports, gen_node.body[1:]) ) ) - gen_node = deduplicate_sorted_imports(gen_node) + gen_node = cdd.shared.ast_utils.deduplicate_sorted_imports(gen_node) cdd.shared.ast_utils.merge_assignment_lists(gen_node, "__all__") if dry_run: print( @@ -586,6 +607,7 @@ def emit_files_from_module_and_return_imports( no_word_wrap, dry_run, filesystem_layout, + extra_modules_to_all, ): """ Emit type `emit_name` of all files in `module_root_dir` into `output_directory` @@ -622,6 +644,9 @@ def emit_files_from_module_and_return_imports( :param filesystem_layout: Hierarchy of folder and file names generated. "java" is file per package per name. :type filesystem_layout: ```Literal["java", "as_input"]``` + :param extra_modules_to_all: Internal arg. Prepended to symbol resolver. E.g., `(("ast", {"List"}),)`. + :type extra_modules_to_all: ```Optional[tuple[tuple[str, frozenset], ...]]``` + :return: List of (mod_name or None, relative_filename_path, ImportFrom) to generated module(s) :rtype: ```list[Tuple[Optional[str], str, ImportFrom]]``` """ @@ -633,6 +658,7 @@ def emit_files_from_module_and_return_imports( mock_imports=mock_imports, filesystem_layout=filesystem_layout, output_directory=output_directory, + extra_modules_to_all=extra_modules_to_all, no_word_wrap=no_word_wrap, dry_run=dry_run, ) diff --git a/cdd/compound/openapi/utils/emit_utils.py b/cdd/compound/openapi/utils/emit_utils.py index 9c164318..a6787331 100644 --- a/cdd/compound/openapi/utils/emit_utils.py +++ b/cdd/compound/openapi/utils/emit_utils.py @@ -2,9 +2,9 @@ Utility functions for `cdd.emit.sqlalchemy` """ -from cdd.sqlalchemy.utils.emit_utils import typ2column_type +import cdd.sqlalchemy.utils.emit_utils -typ2column_type.update( +cdd.sqlalchemy.utils.emit_utils.typ2column_type.update( { "bool": "Boolean", "dict": "JSON", diff --git a/cdd/docstring/utils/emit_utils.py b/cdd/docstring/utils/emit_utils.py index 1672bc45..daf1d27e 100644 --- a/cdd/docstring/utils/emit_utils.py +++ b/cdd/docstring/utils/emit_utils.py @@ -2,7 +2,7 @@ Utility functions for `cdd.emit.docstring` """ -from cdd.shared.ast_utils import NoneStr +import cdd.shared.ast_utils from cdd.shared.defaults_utils import extract_default from cdd.shared.pure_utils import simple_types, unquote @@ -50,7 +50,7 @@ def interpolate_defaults( _param["default"] = ( simple_types[_param["typ"]] if _param.get("typ", memoryview) in simple_types - else NoneStr + else cdd.shared.ast_utils.NoneStr ) return name, _param diff --git a/cdd/function/emit.py b/cdd/function/emit.py index ef2fd1d3..eed0991f 100644 --- a/cdd/function/emit.py +++ b/cdd/function/emit.py @@ -6,8 +6,10 @@ from ast import Expr, FunctionDef, Load, Name, Return, arguments from typing import Optional +import cdd.shared.ast_utils from cdd.docstring.emit import docstring -from cdd.shared.ast_utils import maybe_type_comment, set_arg, set_value + +# import , , from cdd.shared.emit.utils.emitter_utils import get_internal_body from cdd.shared.pure_utils import PY3_8, none_types, simple_types from cdd.shared.types import Internal @@ -84,13 +86,15 @@ def function( function_type: Optional[str] = function_type or intermediate_repr["type"] args = ( - [] if function_type in frozenset((None, "static")) else [set_arg(function_type)] + [] + if function_type in frozenset((None, "static")) + else [cdd.shared.ast_utils.set_arg(function_type)] ) from cdd.shared.emit.utils.emitter_utils import ast_parse_fix args_from_params = list( map( - lambda param: set_arg( + lambda param: cdd.shared.ast_utils.set_arg( annotation=( ( Name(param[1]["typ"], Load(), lineno=None, col_offset=None) @@ -108,9 +112,9 @@ def function( defaults_from_params = list( map( lambda param: ( - set_value(None) + cdd.shared.ast_utils.set_value(None) if param[1].get("default") in none_types - else set_value(param[1].get("default")) + else cdd.shared.ast_utils.set_value(param[1].get("default")) ), params_no_kwargs, ) @@ -148,7 +152,7 @@ def function( kw_defaults=kw_defaults, kwarg=next( map( - lambda param: set_arg(param[0]), + lambda param: cdd.shared.ast_utils.set_arg(param[0]), filter( lambda param: param[0].endswith("kwargs"), intermediate_repr["params"].items(), @@ -166,7 +170,7 @@ def function( None, ( Expr( - set_value( + cdd.shared.ast_utils.set_value( docstring( intermediate_repr, docstring_format=docstring_format, @@ -207,7 +211,7 @@ def function( arguments_args=None, identifier_name=None, stmt=None, - **maybe_type_comment + **cdd.shared.ast_utils.maybe_type_comment ) diff --git a/cdd/function/parse.py b/cdd/function/parse.py index 213f8163..7f954af3 100644 --- a/cdd/function/parse.py +++ b/cdd/function/parse.py @@ -13,10 +13,10 @@ from typing import List, Optional, cast import cdd.docstring.parse +import cdd.shared.ast_utils import cdd.shared.docstring_parsers import cdd.shared.parse.utils.parser_utils from cdd.function.utils.parse_utils import _interpolate_return -from cdd.shared.ast_utils import NoneStr, func_arg2param, get_function_type from cdd.shared.pure_utils import rpartial from cdd.shared.types import IntermediateRepr @@ -104,7 +104,7 @@ def function( function_name=function_name, function_def_name=function_def.name ) - found_type = get_function_type(function_def) + found_type = cdd.shared.ast_utils.get_function_type(function_def) # Read docstring doc_str: Optional[str] = ( @@ -168,7 +168,7 @@ def function( ): _param = intermediate_repr["params"].pop(function_def.args.kwarg.arg) assert "typ" in _param - _param["default"] = NoneStr + _param["default"] = cdd.shared.ast_utils.NoneStr params_to_append[function_def.args.kwarg.arg] = _param del _param @@ -195,7 +195,7 @@ def function( { "params": OrderedDict( ( - func_arg2param( + cdd.shared.ast_utils.func_arg2param( getattr(function_def.args, args)[idx], default=getattr(function_def.args, defaults)[idx], ) diff --git a/cdd/function/utils/emit_utils.py b/cdd/function/utils/emit_utils.py index dfd8f466..c8ce5fff 100644 --- a/cdd/function/utils/emit_utils.py +++ b/cdd/function/utils/emit_utils.py @@ -5,8 +5,8 @@ import ast from ast import Expr, FunctionDef, Return, arguments +import cdd.shared.ast_utils from cdd.class_.utils.emit_utils import RewriteName -from cdd.shared.ast_utils import get_value, maybe_type_comment, set_arg, set_value from cdd.shared.docstring_utils import emit_param_str from cdd.shared.pure_utils import ( code_quoted, @@ -48,7 +48,7 @@ def _make_call_meth(body, return_type, param_names, docstring_format, word_wrap) None if body.get("doc") in none_types else Expr( - set_value( + cdd.shared.ast_utils.set_value( emit_param_str( ( "return_type", @@ -70,12 +70,16 @@ def _make_call_meth(body, return_type, param_names, docstring_format, word_wrap) ( RewriteName(param_names).visit( Return( - get_value(ast.parse(return_type.strip("`")).body[0]), + cdd.shared.ast_utils.get_value( + ast.parse(return_type.strip("`")).body[0] + ), expr=None, ) ) if code_quoted(body["default"]) - else Return(set_value(body["default"]), expr=None) + else Return( + cdd.shared.ast_utils.set_value(body["default"]), expr=None + ) ), ), ) @@ -85,7 +89,7 @@ def _make_call_meth(body, return_type, param_names, docstring_format, word_wrap) ast.fix_missing_locations( FunctionDef( args=arguments( - args=[set_arg("self")], + args=[cdd.shared.ast_utils.set_arg("self")], defaults=[], kw_defaults=[], kwarg=None, @@ -103,7 +107,7 @@ def _make_call_meth(body, return_type, param_names, docstring_format, word_wrap) identifier_name=None, stmt=None, lineno=None, - **maybe_type_comment + **cdd.shared.ast_utils.maybe_type_comment ) ) if body diff --git a/cdd/function/utils/parse_utils.py b/cdd/function/utils/parse_utils.py index 6246ab8a..cc4aa242 100644 --- a/cdd/function/utils/parse_utils.py +++ b/cdd/function/utils/parse_utils.py @@ -6,9 +6,9 @@ from collections import OrderedDict from typing import Optional -from cdd.shared.ast_utils import get_value +import cdd.shared.ast_utils +import cdd.shared.source_transformer from cdd.shared.pure_utils import PY_GTE_3_8, rpartial -from cdd.shared.source_transformer import to_code if PY_GTE_3_8: from cdd.shared.pure_utils import FakeConstant @@ -74,16 +74,20 @@ def _interpolate_return(function_def, intermediate_repr): ) else "```{}```".format(default) ) - )(get_value(get_value(return_ast))) + )( + cdd.shared.ast_utils.get_value( + cdd.shared.ast_utils.get_value(return_ast) + ) + ) ) - )(to_code(return_ast.value).rstrip("\n")) + )(cdd.shared.source_transformer.to_code(return_ast.value).rstrip("\n")) if hasattr(function_def, "returns") and function_def.returns is not None: intermediate_repr["returns"] = intermediate_repr.get("returns") or OrderedDict( (("return_type", {}),) ) - intermediate_repr["returns"]["return_type"]["typ"] = to_code( - function_def.returns - ).rstrip("\n") + intermediate_repr["returns"]["return_type"]["typ"] = ( + cdd.shared.source_transformer.to_code(function_def.returns).rstrip("\n") + ) return intermediate_repr diff --git a/cdd/json_schema/utils/emit_utils.py b/cdd/json_schema/utils/emit_utils.py index 1ac05fe1..3bd5dcdc 100644 --- a/cdd/json_schema/utils/emit_utils.py +++ b/cdd/json_schema/utils/emit_utils.py @@ -6,8 +6,8 @@ from ast import AST, Set from typing import Dict +import cdd.shared.ast_utils from cdd.json_schema.utils.parse_utils import json_type2typ -from cdd.shared.ast_utils import Set_to_set, ast_type_to_python_type, get_value from cdd.shared.pure_utils import none_types @@ -47,13 +47,20 @@ def param2json_schema_property(param, required): required.append(name) if _param["type"].startswith("Literal["): - parsed_typ = get_value(ast.parse(_param["type"]).body[0]) + parsed_typ = cdd.shared.ast_utils.get_value( + ast.parse(_param["type"]).body[0] + ) assert ( parsed_typ.value.id == "Literal" ), "Only basic Literal support is implemented, not {}".format( parsed_typ.value.id ) - enum = sorted(map(get_value, get_value(parsed_typ.slice).elts)) + enum = sorted( + map( + cdd.shared.ast_utils.get_value, + cdd.shared.ast_utils.get_value(parsed_typ.slice).elts, + ) + ) _param.update( { "pattern": "|".join(enum), @@ -63,10 +70,12 @@ def param2json_schema_property(param, required): if _param.get("default", False) in none_types: del _param["default"] # Will be inferred as `null` from the type elif isinstance(_param.get("default"), AST): - _param["default"] = ast_type_to_python_type(_param["default"]) + _param["default"] = cdd.shared.ast_utils.ast_type_to_python_type( + _param["default"] + ) if isinstance(_param.get("choices"), Set): _param["pattern"] = "|".join( - sorted(map(str, Set_to_set(_param.pop("choices")))) + sorted(map(str, cdd.shared.ast_utils.Set_to_set(_param.pop("choices")))) ) return name, _param diff --git a/cdd/json_schema/utils/parse_utils.py b/cdd/json_schema/utils/parse_utils.py index b0b85026..69d1b163 100644 --- a/cdd/json_schema/utils/parse_utils.py +++ b/cdd/json_schema/utils/parse_utils.py @@ -4,7 +4,7 @@ from typing import Dict -from cdd.shared.ast_utils import NoneStr +import cdd.shared.ast_utils from cdd.shared.pure_utils import namespaced_pascal_to_upper_camelcase, none_types @@ -103,7 +103,7 @@ def transform_ref_fk_set(ref, foreign_key): ): _param["typ"] = "Optional[{}]".format(_param["typ"]) if _param.get("default", False) in none_types: - _param["default"] = NoneStr + _param["default"] = cdd.shared.ast_utils.NoneStr return name, _param diff --git a/cdd/shared/ast_utils.py b/cdd/shared/ast_utils.py index caf1e58b..8b228d9b 100644 --- a/cdd/shared/ast_utils.py +++ b/cdd/shared/ast_utils.py @@ -45,15 +45,18 @@ from itertools import chain, filterfalse, groupby from json import dumps from operator import attrgetter, contains, inv, itemgetter, neg, not_, pos +from os import path from typing import FrozenSet, Generator, Optional from typing import __all__ as typing__all__ +import cdd.shared.source_transformer from cdd.shared.defaults_utils import extract_default, needs_quoting from cdd.shared.pure_utils import ( PY_GTE_3_8, PY_GTE_3_9, code_quoted, fill, + find_module_filepath, identity, none_types, paren_wrap_code, @@ -292,6 +295,13 @@ ("sqlalchemy", sqlalchemy___all__), ) # type: tuple[tuple[str, frozenset], ...] +DEFAULT_MODULES_TO_ALL_SQL_FIRST = ( + ("sqlalchemy", sqlalchemy___all__), + ("typing", typing___all__), + ("typing_extensions", typing_extensions___all__), + ("collections.abc", collections_abc___all__), +) # type: tuple[tuple[str, frozenset], ...] + # Was `"globals().__getitem__"`; this type is used for `Any` and any other unhandled FALLBACK_TYP: str = "str" @@ -1898,6 +1908,69 @@ def get_doc_str(node): return get_value(val) +def get_names(node): + """ + Get name(s) from: + - Assign targets + - AnnAssign target + - Function, AsyncFunction, ClassDef + + :param node: AST node + :type node: ```Union[Assign, AnnAssign, Function, AsyncFunctionDef, ClassDef]``` + + :return: All top-level symbols (except those within try/except and if/elif/else blocks) + :rtype: ```Generator[str]``` + """ + if isinstance(node, Assign): + return map(attrgetter("id"), node.targets) + elif isinstance(node, AnnAssign): + return iter((node.target.id,)) + elif isinstance(node, (AsyncFunctionDef, FunctionDef, ClassDef)): + return iter((node.name,)) + return iter(()) + + +def module_to_all(module_or_filepath): + """ + From input, create (("module_name", {"symbol0", "symbol1"}),) + + :param module_or_filepath: Module or filepath + :type module_or_filepath: ```Union[str, Module]``` + + :return: `__all__` from module (if present) else all symbols in module + :rtype: ```List[str]``` + """ + assert isinstance(module_or_filepath, (str, Module)) + if not path.exists(module_or_filepath): + module_or_filepath = find_module_filepath(module_or_filepath) + + with open(module_or_filepath, "rt") as f: + module_or_filepath: Module = cdd.shared.source_transformer.ast_parse( + f.read(), filename=module_or_filepath + ) + + module_or_filepath: Module = module_or_filepath + + # If exists, construct `list[str]` version of `__all__` + all_ = list( + map( + get_value, + chain.from_iterable( + map( + attrgetter("elts"), + map(get_value, get_ass_where_name(module_or_filepath, "__all__")), + ) + ), + ) + ) + + return ( + all_ + if all_ + else list(chain.from_iterable(map(get_names, module_or_filepath.body))) + ) + + def merge_assignment_lists(node, name, unique_sort=True): """ Merge multiple same-name lists within the body of a node into one, e.g., if you have multiple ```__all__``` @@ -2147,10 +2220,7 @@ def get_types(node): ) -def infer_imports( - module, - modules_to_all=DEFAULT_MODULES_TO_ALL, -): +def infer_imports(module, modules_to_all=DEFAULT_MODULES_TO_ALL): """ Infer imports from AST nodes (Name|.annotation|.type_comment); in order; these: - typing @@ -2192,6 +2262,8 @@ def node_to_importable_name(node): else: return None + _symbol_to_import = partial(symbol_to_import, modules_to_all=modules_to_all) + # Lots of room for optimisation here; but its probably NP-hard: imports = tuple( map( @@ -2217,7 +2289,7 @@ def node_to_importable_name(node): None, map( # Because there are duplicate names, centralise all import resolution here and order them - partial(symbol_to_import, modules_to_all=modules_to_all), + _symbol_to_import, sorted( frozenset( chain.from_iterable( @@ -2336,6 +2408,8 @@ def deduplicate_sorted_imports(module): NoneStr = "```(None)```" if PY_GTE_3_9 else "```None```" __all__ = [ + "DEFAULT_MODULES_TO_ALL", + "DEFAULT_MODULES_TO_ALL_SQL_FIRST", "Dict_to_dict", "FALLBACK_ARGPARSE_TYP", "FALLBACK_TYP", diff --git a/cdd/shared/docstring_parsers.py b/cdd/shared/docstring_parsers.py index 394bb6f9..1a692ded 100644 --- a/cdd/shared/docstring_parsers.py +++ b/cdd/shared/docstring_parsers.py @@ -44,9 +44,10 @@ else: from typing_extensions import LiteralString +import cdd.shared.ast_utils +import cdd.shared.source_transformer from cdd.docstring.utils.emit_utils import interpolate_defaults from cdd.docstring.utils.parse_utils import parse_adhoc_doc_for_typ -from cdd.shared.ast_utils import NoneStr, get_value from cdd.shared.defaults_utils import ( _remove_default_from_param, extract_default, @@ -70,7 +71,6 @@ unquote, update_d, ) -from cdd.shared.source_transformer import to_code from cdd.shared.types import IntermediateRepr @@ -543,7 +543,7 @@ def _set_name_and_type(param, infer_type, word_wrap, none_default_for_kwargs=Fal name, _param = param del param was = deepcopy(_param) - was_none = was.get("default") in frozenset((NoneStr, "None")) + was_none = was.get("default") in frozenset((cdd.shared.ast_utils.NoneStr, "None")) if "doc" in _param: merge_present_params( target_param=_param, @@ -613,7 +613,7 @@ def __set_name_and_type_handle_doc_in_param(_param, name, was_none, word_wrap): ).rstrip() typ = parse_adhoc_doc_for_typ( - _param["doc"], name, _param.get("default") == NoneStr + _param["doc"], name, _param.get("default") == cdd.shared.ast_utils.NoneStr ) if typ is not None: try: @@ -641,9 +641,9 @@ def _infer_default(_param, infer_type): :type infer_type: ```bool``` """ if isinstance(_param["default"], (Str, Bytes, Num, ast.Constant, NameConstant)): - _param["default"] = get_value(_param["default"]) + _param["default"] = cdd.shared.ast_utils.get_value(_param["default"]) if _param.get("default", False) in none_types: - _param["default"] = NoneStr + _param["default"] = cdd.shared.ast_utils.NoneStr if infer_type and _param.get("typ") is None and _param["default"] not in none_types: _param["typ"] = type(_param["default"]).__name__ if needs_quoting(_param.get("typ")) or isinstance(_param["default"], str): @@ -655,12 +655,16 @@ def _infer_default(_param, infer_type): _param["typ"] = type(_param["default"]).__name__ except ValueError: _param["default"] = "```{default}```".format( - default=paren_wrap_code(to_code(_param["default"]).rstrip("\n")) + default=paren_wrap_code( + cdd.shared.source_transformer.to_code(_param["default"]).rstrip( + "\n" + ) + ) ) - if _param.get("typ") is None and _param["default"] != NoneStr: + if _param.get("typ") is None and _param["default"] != cdd.shared.ast_utils.NoneStr: _param["typ"] = type(_param["default"]).__name__ if ( - _param["default"] != NoneStr + _param["default"] != cdd.shared.ast_utils.NoneStr and code_quoted(_param["default"]) and "[" not in _param.get( diff --git a/cdd/shared/emit/file.py b/cdd/shared/emit/file.py index 335099e9..50bd4a6c 100644 --- a/cdd/shared/emit/file.py +++ b/cdd/shared/emit/file.py @@ -6,7 +6,7 @@ from importlib import import_module from importlib.util import find_spec -from cdd.shared.source_transformer import to_code +import cdd.shared.source_transformer black = ( import_module("black") @@ -45,7 +45,7 @@ def file(node, filename, mode="a", skip_black=False): """ if not isinstance(node, Module): node: Module = Module(body=[node], type_ignores=[], stmt=None) - src: str = to_code(node) + src: str = cdd.shared.source_transformer.to_code(node) if not skip_black: src = black.format_str( src, diff --git a/cdd/shared/parse/utils/parser_utils.py b/cdd/shared/parse/utils/parser_utils.py index 316a89c3..e01d5781 100644 --- a/cdd/shared/parse/utils/parser_utils.py +++ b/cdd/shared/parse/utils/parser_utils.py @@ -18,10 +18,10 @@ import cdd.class_.parse import cdd.docstring.parse import cdd.function.parse +import cdd.shared.ast_utils import cdd.shared.docstring_parsers import cdd.shared.parse from cdd.class_.utils.parse_utils import get_source -from cdd.shared.ast_utils import get_value from cdd.shared.pure_utils import lstrip_namespace, none_types, rpartial, simple_types from cdd.shared.types import IntermediateRepr @@ -236,7 +236,7 @@ def infer(*args, **kwargs): if not is_supported_ast_node: if not isinstance(node, str): - node = get_value(node) + node = cdd.shared.ast_utils.get_value(node) if ( isinstance(node, str) and not node.startswith("def ") diff --git a/cdd/shared/source_transformer.py b/cdd/shared/source_transformer.py index 7c937637..86966cac 100644 --- a/cdd/shared/source_transformer.py +++ b/cdd/shared/source_transformer.py @@ -7,7 +7,7 @@ from sys import version_info from typing import Optional -from cdd.shared.ast_utils import annotate_ancestry +import cdd.shared.ast_utils from cdd.shared.pure_utils import reindent, tab unparse = ( @@ -62,7 +62,7 @@ def ast_parse( """ parsed_ast = parse(source, filename=filename, mode=mode) if not skip_annotate: - annotate_ancestry(parsed_ast, filename=filename) + cdd.shared.ast_utils.annotate_ancestry(parsed_ast, filename=filename) setattr(parsed_ast, "__file__", filename) if not skip_docstring_remit and isinstance( parsed_ast, (Module, ClassDef, FunctionDef, AsyncFunctionDef) diff --git a/cdd/sqlalchemy/utils/emit_utils.py b/cdd/sqlalchemy/utils/emit_utils.py index c492d1a6..da54a028 100644 --- a/cdd/sqlalchemy/utils/emit_utils.py +++ b/cdd/sqlalchemy/utils/emit_utils.py @@ -41,14 +41,9 @@ from platform import system from typing import Any, Dict, List, Optional +import cdd.shared.ast_utils +import cdd.shared.source_transformer import cdd.sqlalchemy.utils.shared_utils -from cdd.shared.ast_utils import ( - NoneStr, - get_value, - maybe_type_comment, - set_arg, - set_value, -) from cdd.shared.pure_utils import ( find_module_filepath, namespaced_upper_camelcase_to_pascal, @@ -56,7 +51,6 @@ rpartial, tab, ) -from cdd.shared.source_transformer import to_code from cdd.shared.types import ParamVal from cdd.sqlalchemy.utils.parse_utils import ( column_type2typ, @@ -93,7 +87,7 @@ def param_to_sqlalchemy_column_calls(name_param, include_name): args, keywords, nullable, multiple = [], [], None, False if include_name: - args.append(set_value(name)) + args.append(cdd.shared.ast_utils.set_value(name)) x_typ_sql = _param.get("x_typ", {}).get("sql", {}) # type: dict @@ -112,7 +106,11 @@ def param_to_sqlalchemy_column_calls(name_param, include_name): if pk: _param["doc"] = _param["doc"][4:].lstrip() keywords.append( - ast.keyword(arg="primary_key", value=set_value(True), identifier=None), + ast.keyword( + arg="primary_key", + value=cdd.shared.ast_utils.set_value(True), + identifier=None, + ), ) elif fk: end: int = _param["doc"].find("]") + 1 @@ -121,7 +119,7 @@ def param_to_sqlalchemy_column_calls(name_param, include_name): args.append( Call( func=Name("ForeignKey", Load(), lineno=None, col_offset=None), - args=[set_value(fk_val)], + args=[cdd.shared.ast_utils.set_value(fk_val)], keywords=[], lineno=None, col_offset=None, @@ -135,13 +133,19 @@ def param_to_sqlalchemy_column_calls(name_param, include_name): if rstripped_dot_doc: doc_added_at: int = len(keywords) keywords.append( - ast.keyword(arg="doc", value=set_value(rstripped_dot_doc), identifier=None) + ast.keyword( + arg="doc", + value=cdd.shared.ast_utils.set_value(rstripped_dot_doc), + identifier=None, + ) ) if x_typ_sql.get("constraints"): keywords += [ ast.keyword( - arg=k, value=v if isinstance(v, AST) else set_value(v), identifier=None + arg=k, + value=v if isinstance(v, AST) else cdd.shared.ast_utils.set_value(v), + identifier=None, ) for k, v in _param["x_typ"]["sql"]["constraints"].items() ] @@ -151,7 +155,9 @@ def param_to_sqlalchemy_column_calls(name_param, include_name): keywords.append( ast.keyword( arg="comment", - value=set_value("[schema={}]".format(dumps(_param["ir"]))), + value=cdd.shared.ast_utils.set_value( + "[schema={}]".format(dumps(_param["ir"])) + ), identifier=None, ) ) @@ -164,7 +170,9 @@ def param_to_sqlalchemy_column_calls(name_param, include_name): value=( default if isinstance(default, AST) - else set_value(None if default == NoneStr else default) + else cdd.shared.ast_utils.set_value( + None if default == cdd.shared.ast_utils.NoneStr else default + ) ), identifier=None, ) @@ -172,7 +180,11 @@ def param_to_sqlalchemy_column_calls(name_param, include_name): if isinstance(nullable, bool): keywords.append( - ast.keyword(arg="nullable", value=set_value(nullable), identifier=None) + ast.keyword( + arg="nullable", + value=cdd.shared.ast_utils.set_value(nullable), + identifier=None, + ) ) # if include_name is True and _param.get("doc") and _param["doc"] != "[PK]": @@ -323,7 +335,7 @@ def generate_repr_method(params, cls_name, docstring_format): args=arguments( posonlyargs=[], arg=None, - args=[set_arg("self")], + args=[cdd.shared.ast_utils.set_arg("self")], kwonlyargs=[], kw_defaults=[], defaults=[], @@ -332,7 +344,7 @@ def generate_repr_method(params, cls_name, docstring_format): ), body=[ Expr( - set_value( + cdd.shared.ast_utils.set_value( """\n{sep}{_repr_docstring}""".format( sep=tab * 2, _repr_docstring=( @@ -348,7 +360,7 @@ def generate_repr_method(params, cls_name, docstring_format): Return( value=Call( func=Attribute( - set_value( + cdd.shared.ast_utils.set_value( "{cls_name}({format_args})".format( cls_name=cls_name, format_args=", ".join( @@ -393,7 +405,7 @@ def generate_repr_method(params, cls_name, docstring_format): stmt=None, lineno=None, returns=None, - **maybe_type_comment, + **cdd.shared.ast_utils.maybe_type_comment, ) @@ -420,7 +432,7 @@ def generate_create_from_attr_staticmethod(params, cls_name, docstring_format): args=arguments( posonlyargs=[], arg=None, - args=[set_arg("record")], + args=[cdd.shared.ast_utils.set_arg("record")], kwonlyargs=[], kw_defaults=[], defaults=[], @@ -429,7 +441,7 @@ def generate_create_from_attr_staticmethod(params, cls_name, docstring_format): ), body=[ Expr( - set_value( + cdd.shared.ast_utils.set_value( """\n{sep}{_repr_docstring}""".format( sep=tab * 2, _repr_docstring=( @@ -489,7 +501,11 @@ def generate_create_from_attr_staticmethod(params, cls_name, docstring_format): col_offset=None, ), iter=Tuple( - elts=list(map(set_value, keys)), + elts=list( + map( + cdd.shared.ast_utils.set_value, keys + ) + ), ctx=Load(), lineno=None, col_offset=None, @@ -516,14 +532,18 @@ def generate_create_from_attr_staticmethod(params, cls_name, docstring_format): lineno=None, col_offset=None, ), - set_value(None), + cdd.shared.ast_utils.set_value( + None + ), ], keywords=[], lineno=None, col_offset=None, ), ops=[IsNot()], - comparators=[set_value(None)], + comparators=[ + cdd.shared.ast_utils.set_value(None) + ], lineno=None, col_offset=None, ) @@ -551,7 +571,7 @@ def generate_create_from_attr_staticmethod(params, cls_name, docstring_format): stmt=None, lineno=None, returns=None, - **maybe_type_comment, + **cdd.shared.ast_utils.maybe_type_comment, ) @@ -596,15 +616,17 @@ def generate_create_from_attr_staticmethod(params, cls_name, docstring_format): args=[ Subscript( value=Name("environ", Load(), lineno=None, col_offset=None), - slice=set_value("RDBMS_URI"), + slice=cdd.shared.ast_utils.set_value("RDBMS_URI"), ctx=Load(), ) ], - keywords=[keyword(arg="echo", value=set_value(True))], + keywords=[ + keyword(arg="echo", value=cdd.shared.ast_utils.set_value(True)) + ], ), expr=None, lineno=None, - **maybe_type_comment, + **cdd.shared.ast_utils.maybe_type_comment, ), Assign( targets=[Name("metadata", Store(), lineno=None, col_offset=None)], @@ -615,7 +637,7 @@ def generate_create_from_attr_staticmethod(params, cls_name, docstring_format): ), expr=None, lineno=None, - **maybe_type_comment, + **cdd.shared.ast_utils.maybe_type_comment, ), ClassDef( name="Base", @@ -627,7 +649,7 @@ def generate_create_from_attr_staticmethod(params, cls_name, docstring_format): value=Name("metadata", Load(), lineno=None, col_offset=None), expr=None, lineno=None, - **maybe_type_comment, + **cdd.shared.ast_utils.maybe_type_comment, ) ], decorator_list=[], @@ -640,18 +662,22 @@ def generate_create_from_attr_staticmethod(params, cls_name, docstring_format): Assign( targets=[Name("__all__", Store(), lineno=None, col_offset=None)], value=AST_List( - elts=list(map(set_value, ("Base", "metadata", "engine"))), + elts=list( + map(cdd.shared.ast_utils.set_value, ("Base", "metadata", "engine")) + ), ctx=Load(), ), expr=None, lineno=None, - **maybe_type_comment, + **cdd.shared.ast_utils.maybe_type_comment, ), ], type_ignores=[], ) -mock_engine_base_metadata_str = to_code(mock_engine_base_metadata_mod) +mock_engine_base_metadata_str = cdd.shared.source_transformer.to_code( + mock_engine_base_metadata_mod +) def generate_create_tables_mod(module_name): @@ -690,7 +716,7 @@ def generate_create_tables_mod(module_name): col_offset=None, ), ops=[Eq()], - comparators=[set_value("__main__")], + comparators=[cdd.shared.ast_utils.set_value("__main__")], ), body=[ Expr( @@ -702,7 +728,9 @@ def generate_create_tables_mod(module_name): col_offset=None, ), args=[ - set_value("Base.metadata.create_all for"), + cdd.shared.ast_utils.set_value( + "Base.metadata.create_all for" + ), Attribute( value=Name( "engine", @@ -713,7 +741,7 @@ def generate_create_tables_mod(module_name): attr="name", ctx=Load(), ), - set_value(";"), + cdd.shared.ast_utils.set_value(";"), ], keywords=[], ) @@ -838,7 +866,7 @@ def update_with_imports_from_columns(filename): ) with open(filename, "wt") as f: - f.write(to_code(mod)) + f.write(cdd.shared.source_transformer.to_code(mod)) def update_fk_for_file(filename): @@ -939,7 +967,7 @@ def handle_sqlalchemy_cls(symbol_to_module, sqlalchemy_class_def): ) with open(filename, "wt") as f: - f.write(to_code(mod)) + f.write(cdd.shared.source_transformer.to_code(mod)) def rewrite_fk(symbol_to_module, column_assign): @@ -971,7 +999,7 @@ def rewrite_fk(symbol_to_module, column_assign): and isinstance(column_assign.value.func, Name) and column_assign.value.func.id == "Column" ), 'Expected `Call.func.Name.id` of " = Column" eval to ` = Column(...)` got `{code}`'.format( - code=to_code(column_assign).rstrip() + code=cdd.shared.source_transformer.to_code(column_assign).rstrip() ) def rewrite_fk_from_import(column_name, foreign_key_call): @@ -995,7 +1023,7 @@ def rewrite_fk_from_import(column_name, foreign_key_call): and isinstance(foreign_key_call.func, Name) and foreign_key_call.func.id == "ForeignKey" ), 'Expected `Call.func.Name.id` of "ForeignKey" eval to `ForeignKey(...)` got `{code}`'.format( - code=to_code(foreign_key_call).rstrip() + code=cdd.shared.source_transformer.to_code(foreign_key_call).rstrip() ) if column_name.id in symbol_to_module: with open( @@ -1016,7 +1044,11 @@ def rewrite_fk_from_import(column_name, foreign_key_call): del pk_typ return Name(typ, Load(), lineno=None, col_offset=None), Call( func=Name("ForeignKey", Load(), lineno=None, col_offset=None), - args=[set_value(".".join((get_table_name(matching_class), pk)))], + args=[ + cdd.shared.ast_utils.set_value( + ".".join((get_table_name(matching_class), pk)) + ) + ], keywords=[], lineno=None, col_offset=None, @@ -1072,7 +1104,7 @@ def sqlalchemy_class_to_table(class_def, parse_original_whitespace): # Parse into the same format that `sqlalchemy_table` can read, then return with a call to it - name: str = get_value( + name: str = cdd.shared.ast_utils.get_value( next( filter( lambda assign: any( @@ -1099,7 +1131,9 @@ def _merge_name_to_column(assign): :return: Unwrapped Call with name prepended :rtype: ```Call``` """ - assign.value.args.insert(0, set_value(assign.targets[0].id)) + assign.value.args.insert( + 0, cdd.shared.ast_utils.set_value(assign.targets[0].id) + ) return assign.value return Call( @@ -1107,7 +1141,10 @@ def _merge_name_to_column(assign): args=list( chain.from_iterable( ( - (set_value(name), Name("metadata_obj", Load())), + ( + cdd.shared.ast_utils.set_value(name), + Name("metadata_obj", Load()), + ), map( _merge_name_to_column, filterfalse( @@ -1129,7 +1166,13 @@ def _merge_name_to_column(assign): keywords=( [] if doc_string is None - else [keyword(arg="comment", value=set_value(doc_string), identifier=None)] + else [ + keyword( + arg="comment", + value=cdd.shared.ast_utils.set_value(doc_string), + identifier=None, + ) + ] ), expr=None, expr_func=None, @@ -1167,17 +1210,21 @@ def sqlalchemy_table_to_class(table_expr_ass): col_offset=None, ) ], - value=set_value(get_value(table_expr_ass.value.args[0])), + value=cdd.shared.ast_utils.set_value( + cdd.shared.ast_utils.get_value( + table_expr_ass.value.args[0] + ) + ), expr=None, lineno=None, - **maybe_type_comment, + **cdd.shared.ast_utils.maybe_type_comment, ), ), map( lambda column_call: Assign( targets=[ Name( - get_value(column_call.args[0]), + cdd.shared.ast_utils.get_value(column_call.args[0]), Store(), lineno=None, col_offset=None, @@ -1196,7 +1243,7 @@ def sqlalchemy_table_to_class(table_expr_ass): ), expr=None, lineno=None, - **maybe_type_comment, + **cdd.shared.ast_utils.maybe_type_comment, ), filter( lambda node: isinstance(node, Call) diff --git a/cdd/sqlalchemy/utils/parse_utils.py b/cdd/sqlalchemy/utils/parse_utils.py index 3dee19ac..72833114 100644 --- a/cdd/sqlalchemy/utils/parse_utils.py +++ b/cdd/sqlalchemy/utils/parse_utils.py @@ -8,11 +8,11 @@ from operator import attrgetter from typing import FrozenSet -from cdd.shared.ast_utils import get_value +import cdd.shared.ast_utils +import cdd.shared.source_transformer from cdd.shared.pure_utils import PY_GTE_3_8 from cdd.shared.pure_utils import FakeConstant as Str from cdd.shared.pure_utils import append_to_dict, indent_all_but_first, rpartial, tab -from cdd.shared.source_transformer import to_code if PY_GTE_3_8: pass @@ -124,13 +124,19 @@ def column_parse_arg(idx_arg): elif isinstance(arg, Call): func_id = arg.func.id.rpartition(".")[2] if func_id == "Enum": - return "typ", "Literal{}".format(list(map(get_value, arg.args))) + return "typ", "Literal{}".format( + list(map(cdd.shared.ast_utils.get_value, arg.args)) + ) elif func_id == "ForeignKey": - return "foreign_key", ",".join(map(get_value, arg.args)) + return "foreign_key", ",".join( + map(cdd.shared.ast_utils.get_value, arg.args) + ) else: - return "typ", to_code(idx_arg[1]).replace("\n", "") + return "typ", cdd.shared.source_transformer.to_code(idx_arg[1]).replace( + "\n", "" + ) - val = get_value(arg) + val = cdd.shared.ast_utils.get_value(arg) assert val != arg, "Unable to parse {!r}".format(arg) if idx == 0: return None # Column name @@ -146,12 +152,14 @@ def column_parse_kwarg(key_word): :rtype: ```tuple[str, Any]``` """ - val = get_value(key_word.value) + val = cdd.shared.ast_utils.get_value(key_word.value) # Checking that the keyword.value has a value OR is a function call. assert val != key_word.value or isinstance( key_word.value, Call - ), "Unable to parse {!r} of {}".format(key_word.arg, to_code(key_word.value)) + ), "Unable to parse {!r} of {}".format( + key_word.arg, cdd.shared.source_transformer.to_code(key_word.value) + ) return key_word.arg, val @@ -169,7 +177,7 @@ def column_call_to_param(call): assert ( len(call.args) < 4 ), "Complex column parsing not implemented for: Column({})".format( - ", ".join(map(repr, map(get_value, call.args))) + ", ".join(map(repr, map(cdd.shared.ast_utils.get_value, call.args))) ) _param = dict( @@ -219,12 +227,12 @@ def _handle_null(): if ( "default" in _param - and not get_value(call.args[0]).endswith("kwargs") + and not cdd.shared.ast_utils.get_value(call.args[0]).endswith("kwargs") and "doc" in _param ): _param["doc"] += "." - return get_value(call.args[0]), _param + return cdd.shared.ast_utils.get_value(call.args[0]), _param def column_call_name_manipulator(call, operation="remove", name=None): @@ -345,7 +353,7 @@ def get_pk_and_type(sqlalchemy_class): lambda assign: any( filter( lambda key_word: key_word.arg == "primary_key" - and get_value(key_word.value) is True, + and cdd.shared.ast_utils.get_value(key_word.value) is True, assign.value.keywords, ) ), @@ -373,7 +381,7 @@ def get_table_name(sqlalchemy_class): """ return next( map( - lambda assign: get_value(assign.value), + lambda assign: cdd.shared.ast_utils.get_value(assign.value), filter( lambda node: next( filter(lambda target: target.id == "__tablename__", node.targets), diff --git a/cdd/sqlalchemy/utils/shared_utils.py b/cdd/sqlalchemy/utils/shared_utils.py index 09163720..a8e09221 100644 --- a/cdd/sqlalchemy/utils/shared_utils.py +++ b/cdd/sqlalchemy/utils/shared_utils.py @@ -8,10 +8,10 @@ from typing import Optional, cast import cdd.compound.openapi.utils.emit_utils +import cdd.shared.ast_utils +import cdd.shared.source_transformer import cdd.sqlalchemy.utils.emit_utils -from cdd.shared.ast_utils import NoneStr, get_value, set_value from cdd.shared.pure_utils import PY_GTE_3_9, rpartial -from cdd.shared.source_transformer import to_code def _update_args_infer_typ_sqlalchemy_for_scalar(_param, args, x_typ_sql): @@ -37,9 +37,15 @@ def _update_args_infer_typ_sqlalchemy_for_scalar(_param, args, x_typ_sql): args.append( Call( func=Name(type_name, Load(), lineno=None, col_offset=None), - args=list(map(set_value, x_typ_sql.get("type_args", iter(())))), + args=list( + map( + cdd.shared.ast_utils.set_value, x_typ_sql.get("type_args", iter(())) + ) + ), keywords=[ - keyword(arg=arg, value=set_value(val), identifier=None) + keyword( + arg=arg, value=cdd.shared.ast_utils.set_value(val), identifier=None + ) for arg, val in x_typ_sql.get("type_kwargs", dict()).items() ], expr=None, @@ -73,22 +79,28 @@ def update_args_infer_typ_sqlalchemy(_param, args, name, nullable, x_typ_sql): :rtype: ```Tuple[bool, Optional[Union[List[AST], Tuple[AST]]]]``` """ if _param["typ"] is None: - return _param.get("default") == NoneStr, None + return _param.get("default") == cdd.shared.ast_utils.NoneStr, None if _param["typ"].startswith("Optional["): _param["typ"] = _param["typ"][len("Optional[") : -1] nullable = True if "Literal[" in _param["typ"]: - parsed_typ: Call = get_value(ast.parse(_param["typ"]).body[0]) + parsed_typ: Call = cdd.shared.ast_utils.get_value( + ast.parse(_param["typ"]).body[0] + ) if parsed_typ.value.id != "Literal": return nullable, parsed_typ.value - val = get_value(parsed_typ.slice) + val = cdd.shared.ast_utils.get_value(parsed_typ.slice) ( args.append( Call( func=Name("Enum", Load(), lineno=None, col_offset=None), args=val.elts, keywords=[ - ast.keyword(arg="name", value=set_value(name), identifier=None) + ast.keyword( + arg="name", + value=cdd.shared.ast_utils.set_value(name), + identifier=None, + ) ], expr=None, expr_func=None, @@ -119,7 +131,7 @@ def update_args_infer_typ_sqlalchemy(_param, args, name, nullable, x_typ_sql): filter(rpartial(isinstance, Name), ast.walk(list_typ.value.slice)), None ) assert name is not None, "Could not find a type in {!r}".format( - to_code(list_typ.value.slice) + cdd.shared.source_transformer.to_code(list_typ.value.slice) ) args.append( Call( diff --git a/cdd/tests/test_compound/test_exmod.py b/cdd/tests/test_compound/test_exmod.py index c5243b74..47721826 100644 --- a/cdd/tests/test_compound/test_exmod.py +++ b/cdd/tests/test_compound/test_exmod.py @@ -98,9 +98,10 @@ def test_exmod(self) -> None: blacklist=tuple(), whitelist=tuple(), mock_imports=True, - emit_base_engine_metadata=False, + emit_sqlalchemy_submodule=False, output_directory=new_module_dir, target_module_name="gold", + extra_modules=None, no_word_wrap=None, recursive=False, dry_run=False, @@ -126,9 +127,10 @@ def test_exmod_blacklist(self) -> None: blacklist=(".".join((existent_module_dir,) * 2),), whitelist=tuple(), mock_imports=True, - emit_base_engine_metadata=False, + emit_sqlalchemy_submodule=False, output_directory=new_module_dir, target_module_name="gold", + extra_modules=None, no_word_wrap=None, recursive=False, dry_run=False, @@ -158,9 +160,10 @@ def test_exmod_whitelist(self) -> None: blacklist=tuple(), whitelist=(".".join((self.package_root_name, "gen")),), mock_imports=True, - emit_base_engine_metadata=False, + emit_sqlalchemy_submodule=False, output_directory=new_module_dir, target_module_name="gold", + extra_modules=None, no_word_wrap=None, recursive=False, dry_run=False, @@ -207,10 +210,11 @@ def test_exmod_module_directory(self) -> None: blacklist=tuple(), whitelist=tuple(), mock_imports=True, - emit_base_engine_metadata=False, + emit_sqlalchemy_submodule=False, output_directory=path.join(tempdir, "nonexistent"), target_module_name="gold", recursive=False, + extra_modules=None, no_word_wrap=None, dry_run=False, ) @@ -224,10 +228,11 @@ def test_exmod_no_module(self) -> None: blacklist=tuple(), whitelist=tuple(), mock_imports=True, - emit_base_engine_metadata=False, + emit_sqlalchemy_submodule=False, output_directory=path.join(tempdir, "nonexistent"), target_module_name="gold", recursive=False, + extra_modules=None, no_word_wrap=None, dry_run=False, ) @@ -244,10 +249,11 @@ def test_exmod_output_directory_nonexistent(self) -> None: blacklist=tuple(), whitelist=tuple(), mock_imports=True, - emit_base_engine_metadata=False, + emit_sqlalchemy_submodule=False, output_directory=output_directory, target_module_name="gold", recursive=False, + extra_modules=None, no_word_wrap=None, dry_run=False, ) @@ -275,10 +281,11 @@ def test_exmod_dry_run(self) -> None: blacklist=tuple(), whitelist=tuple(), mock_imports=True, - emit_base_engine_metadata=False, + emit_sqlalchemy_submodule=False, output_directory=new_module_dir, target_module_name="gold", recursive=False, + extra_modules=None, no_word_wrap=None, dry_run=True, ) diff --git a/cdd/tests/test_compound/test_exmod_utils.py b/cdd/tests/test_compound/test_exmod_utils.py index d2fb1c18..55cc5ab1 100644 --- a/cdd/tests/test_compound/test_exmod_utils.py +++ b/cdd/tests/test_compound/test_exmod_utils.py @@ -30,13 +30,14 @@ def test_emit_file_on_hierarchy_dry_run(self) -> None: "cdd.compound.exmod_utils.EXMOD_OUT_STREAM", new_callable=StringIO ) as f: emit_file_on_hierarchy( - ("", "foo_dir", ir), - "argparse", - "", - "", - True, - None, - "", + name_orig_ir=("", "foo_dir", ir), + emit_name="argparse", + module_name="", + new_module_name="", + mock_imports=True, + filesystem_layout=None, + extra_modules_to_all=None, + output_directory="", no_word_wrap=None, dry_run=True, ) @@ -66,6 +67,7 @@ def test_emit_file_on_hierarchy(self) -> None: output_directory=tempdir, no_word_wrap=None, dry_run=False, + extra_modules_to_all=None, ) self.assertTrue(path.isdir(tempdir)) @@ -78,9 +80,9 @@ def test__emit_symbols_isfile_emit_filename_true(self) -> None: ) as func__merge_modules, patch( "cdd.shared.ast_utils.merge_assignment_lists", MagicMock() ) as func__merge_assignment_lists, patch( - "cdd.compound.exmod_utils.infer_imports", MagicMock() + "cdd.shared.ast_utils.infer_imports", MagicMock() ) as func__infer_imports, patch( - "cdd.compound.exmod_utils.deduplicate_sorted_imports", MagicMock() + "cdd.shared.ast_utils.deduplicate_sorted_imports", MagicMock() ) as func__deduplicate_sorted_imports: _emit_symbol( name_orig_ir=("", "", dict()), @@ -97,6 +99,7 @@ def test__emit_symbols_isfile_emit_filename_true(self) -> None: isfile_emit_filename=True, name="", mock_imports=True, + extra_modules_to_all=None, no_word_wrap=None, dry_run=True, ) diff --git a/cdd/tests/test_shared/test_ast_utils.py b/cdd/tests/test_shared/test_ast_utils.py index 0629e10d..cee011ec 100644 --- a/cdd/tests/test_shared/test_ast_utils.py +++ b/cdd/tests/test_shared/test_ast_utils.py @@ -38,6 +38,7 @@ from typing import Optional, Union from unittest import TestCase +import cdd.tests.utils_for_tests from cdd.shared.ast_utils import ( NoneStr, RewriteAtQuery, @@ -54,6 +55,7 @@ get_ass_where_name, get_at_root, get_function_type, + get_names, get_types, get_value, infer_imports, @@ -61,6 +63,7 @@ maybe_type_comment, merge_assignment_lists, merge_modules, + module_to_all, node_to_dict, optimise_imports, param2argparse_param, @@ -369,6 +372,61 @@ def test_get_at_root(self) -> None: ) ) + def test_get_names(self) -> None: + """Check the `get_names` works""" + self.assertTupleEqual( + tuple( + get_names( + FunctionDef( + body=[], + name="func_foo", + arguments_args=None, + identifier_name=None, + stmt=None, + ) + ) + ), + ("func_foo",), + ) + + self.assertTupleEqual( + tuple( + get_names( + Assign( + targets=[Name("my_ass", Store(), lineno=None, col_offset=None)], + value=set_value("my_val"), + expr=None, + lineno=None, + **maybe_type_comment, + ) + ) + ), + ("my_ass",), + ) + + self.assertTupleEqual( + tuple( + get_names( + AnnAssign( + annotation=Name("str", Load(), lineno=None, col_offset=None), + simple=1, + target=Name( + "my_ann_ass", Store(), lineno=None, col_offset=None + ), + value=set_value( + "my_ann_ass_val", + ), + expr=None, + expr_annotation=None, + expr_target=None, + col_offset=None, + lineno=None, + ) + ) + ), + ("my_ann_ass",), + ) + def test_infer_imports_with_sqlalchemy(self) -> None: """ Test that `infer_imports` can infer imports for SQLalchemy @@ -1426,6 +1484,34 @@ def test_merge_modules(self) -> None: ) ) + def test_module_to_all(self) -> None: + """Tests that `module_to_all` behaves correctly""" + self.assertListEqual( + module_to_all("cdd.tests.utils_for_tests"), + cdd.tests.utils_for_tests.__all__, + ) + self.assertListEqual( + cdd.tests.utils_for_tests.__all__, + [ + "inspectable_compile", + "mock_function", + # "module_from_file", + "reindent_docstring", + "remove_args_from_docstring", + "replace_docstring", + "run_ast_test", + "run_cli_test", + "unittest_main", + ], + ) + self.assertListEqual( + module_to_all("cdd.tests.test_shared.test_ast_utils"), + [self.__class__.__name__], + ) + self.assertListEqual( + module_to_all("cdd.tests.test_shared.test_ast_utils"), ["TestAstUtils"] + ) + def test_optimise_imports(self) -> None: """Tests that `optimise_imports` deduplicates""" run_ast_test( diff --git a/cdd/tests/utils_for_tests.py b/cdd/tests/utils_for_tests.py index 025c03d7..2e3a97c4 100644 --- a/cdd/tests/utils_for_tests.py +++ b/cdd/tests/utils_for_tests.py @@ -430,7 +430,7 @@ def remove_args_from_docstring(doc_str): ): stack.append(line) in_args = False - return "{}{}".format("\n".join(stack), "") # ("\n" if stack[-1] == "" else "") + return "\n".join(stack) # + ("\n" if stack[-1] == "" else "") __all__ = [