From 012b7a32cf3ce95457462096be0477014cda9cf8 Mon Sep 17 00:00:00 2001 From: Camille <78221213+clatapie@users.noreply.github.com> Date: Wed, 4 Dec 2024 15:38:34 +0100 Subject: [PATCH] maint: general structure (#356) --- .flake8 | 2 +- _package/.pre-commit-config.yaml | 76 +++++++++ config.yaml | 25 ++- doc/source/conf.py | 16 +- src/pyconverter/xml2py/ast_tree.py | 113 +++++++++---- src/pyconverter/xml2py/cli.py | 37 +++-- src/pyconverter/xml2py/custom_functions.py | 109 +++++++++++-- src/pyconverter/xml2py/directory_format.py | 4 +- src/pyconverter/xml2py/formatter.py | 18 ++- src/pyconverter/xml2py/writer.py | 40 +++-- tests/conftest.py | 6 +- tests/customized_functions/inquire.py | 180 +++++++++++++++++++++ tests/test_cli.py | 4 +- tests/test_custom_functions.py | 4 + tests/test_writer.py | 2 +- 15 files changed, 543 insertions(+), 93 deletions(-) create mode 100644 _package/.pre-commit-config.yaml create mode 100644 tests/customized_functions/inquire.py diff --git a/.flake8 b/.flake8 index 0be167fd1..aef76c154 100644 --- a/.flake8 +++ b/.flake8 @@ -1,5 +1,5 @@ [flake8] -exclude = venv, __init__.py, doc/_build, .venv +exclude = venv, __init__.py, doc/_build, .venv, tests/customized_functions/* select = W191, W291, W293, W391, E115, E117, E122, E124, E125, E225, E301, E303, E501, F401, F403 count = True max-complexity = 10 diff --git a/_package/.pre-commit-config.yaml b/_package/.pre-commit-config.yaml new file mode 100644 index 000000000..aef388763 --- /dev/null +++ b/_package/.pre-commit-config.yaml @@ -0,0 +1,76 @@ +fail_fast: True + +ci: + # Commit name when auto fixing PRs. + autofix_commit_msg: | + ci: auto fixes from pre-commit.com hooks. + + for more information, see https://pre-commit.ci + + # PR name when autoupdate + autoupdate_commit_msg: 'ci: pre-commit autoupdate' + + +repos: + +- repo: https://github.com/pycqa/isort + rev: 5.13.2 + hooks: + - id: isort + +- repo: https://github.com/numpy/numpydoc + rev: v1.8.0 + hooks: + - id: numpydoc-validation + exclude: | + (?x)( + tests/| + examples| + doc/source/| + src/ansys/mapdl/core/_commands| + src/ansys/mapdl/core/commands + ) + +- repo: https://github.com/psf/black + rev: 24.10.0 # If version changes --> modify "blacken-docs" manually as well. + hooks: + - id: black + args: + - --line-length=88 + +- repo: https://github.com/adamchainz/blacken-docs + rev: 1.19.0 + hooks: + - id: blacken-docs + additional_dependencies: [black==24.10.0] + +- repo: https://github.com/PyCQA/flake8 + rev: 7.1.1 + hooks: + - id: flake8 + +- repo: https://github.com/codespell-project/codespell + rev: v2.3.0 + hooks: + - id: codespell + args: ["--toml", "pyproject.toml"] + additional_dependencies: ["tomli"] + +# - repo: https://github.com/pycqa/pydocstyle +# rev: 6.1.1 +# hooks: +# - id: pydocstyle +# additional_dependencies: [toml] +# exclude: "tests/" + +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: check-merge-conflict + - id: debug-statements + +# this validates our github workflow files +- repo: https://github.com/python-jsonschema/check-jsonschema + rev: 0.29.4 + hooks: + - id: check-github-workflows diff --git a/config.yaml b/config.yaml index e921bf4b6..f788f5df2 100644 --- a/config.yaml +++ b/config.yaml @@ -2,6 +2,10 @@ library_name_structured: # Future name of the library - pyconverter - generatedcommands +subfolder: + - subfolder + - subsubfolder + new_package_name: package rules: @@ -11,6 +15,25 @@ rules: specific_command_mapping: "*DEL": stardel "C***": c + "/INQUIRE": inquire + +ignored_commands: + - "*VWR" + - "*MWR" + - "C***" + - "*CFO" + - "*CRE" + - "*END" + - "/EOF" + - "*ASK" + - "*IF" + - "*ELSE" + - "CMAT" + - "*REP" + - "*RETURN" + - "LSRE" + specific_classes: - 2D to 3D Analysis: Analysis 2D to 3D \ No newline at end of file + 2D to 3D Analysis: Analysis 2D to 3D + Parameters: Parameter definition \ No newline at end of file diff --git a/doc/source/conf.py b/doc/source/conf.py index 835767354..b146348cf 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -10,7 +10,7 @@ copyright = f"(c) {datetime.now().year} ANSYS, Inc. All rights reserved" author = "ANSYS, Inc." release = version = __version__ -cname = os.getenv("DOCUMENTATION_CNAME", "") +cname = os.getenv("DOCUMENTATION_CNAME", "pyconverter-xml2py.docs.pyansys.com") switcher_version = get_version_match(__version__) REPOSITORY_NAME = "pyconverter-xml2py" @@ -38,7 +38,7 @@ "icon_links": [ { "name": "Support", - "url": "https://github.com/ansys/pyconverter-xml2py/discussions", + "url": f"https://github.com/{USERNAME}/{REPOSITORY_NAME}/discussions", "icon": "fa fa-comment fa-fw", }, ], @@ -54,9 +54,9 @@ html_context = { "display_github": True, # Integrate GitHub - "github_user": "ansys", + "github_user": USERNAME, "github_repo": REPOSITORY_NAME, - "github_version": "main", + "github_version": BRANCH, "doc_path": "doc/source", } @@ -65,13 +65,13 @@ "ansys_sphinx_theme.extension.autoapi", "jupyter_sphinx", "numpydoc", - "sphinx.ext.autodoc", "sphinx_autodoc_typehints", - "sphinx.ext.intersphinx", - "sphinx.ext.extlinks", - "sphinx.ext.graphviz", "sphinx_copybutton", "sphinx_design", + "sphinx.ext.autodoc", + "sphinx.ext.extlinks", + "sphinx.ext.graphviz", + "sphinx.ext.intersphinx", ] # Intersphinx mapping diff --git a/src/pyconverter/xml2py/ast_tree.py b/src/pyconverter/xml2py/ast_tree.py index 6c9fe3083..a256314da 100644 --- a/src/pyconverter/xml2py/ast_tree.py +++ b/src/pyconverter/xml2py/ast_tree.py @@ -28,6 +28,7 @@ from inflect import engine from lxml.etree import tostring from lxml.html import fromstring +from pyconverter.xml2py.custom_functions import CustomFunctions from pyconverter.xml2py.utils.utils import is_numeric, split_trail_alpha import regex as re @@ -79,9 +80,6 @@ # Map XML command to pycommand function NAME_MAP_GLOB = {} -# XML commands to skip -SKIP = {"*IF", "*ELSE", "C***", "*RETURN"} - NO_RESIZE_LIST = ["Variablelist"] @@ -198,6 +196,21 @@ def is_elipsis(name: str) -> bool: return False +def str_types(types, join_str: str) -> str: + """String representation of the parameter types.""" + ptype_str = join_str.join([parm_type.__name__ for parm_type in types]) + return ptype_str + + +def to_py_signature(py_arg_name, types) -> str: + """Return the Python signature of the argument.""" + if py_arg_name not in ["--", "–", ""]: + kwarg = f'{py_arg_name}: {str_types(types, " | ")} = ""' + else: + kwarg = None + return kwarg + + # ############################################################################ # Element class # ############################################################################ @@ -2317,11 +2330,6 @@ def types(self) -> List[type]: return parm_types - def str_types(self, join_str: str) -> str: - """String representation of the parameter types.""" - ptype_str = join_str.join([parm_type.__name__ for parm_type in self.types]) - return ptype_str - def resized_description( self, description: str | None = None, max_length: int = 100, indent: str = "" ) -> List[str]: @@ -2343,7 +2351,7 @@ def to_py_docstring( ) -> List[str]: """Return a list of string to enable converting the element to an RST format.""" if self.py_arg_name not in ["--", "–", ""]: - docstring = [f'{indent}{self.py_arg_name} : {self.str_types(" or ")}'] + docstring = [f'{indent}{self.py_arg_name} : {str_types(self.types, " or ")}'] if isinstance(self._description, str): rst_description = self._description else: @@ -2363,20 +2371,12 @@ def to_py_docstring( rst_description = textwrap.indent(rst_description, description_indent) list_description = rst_description.split("\n") - docstring = [f'{indent}{self.py_arg_name} : {self.str_types(" or ")}'] + docstring = [f'{indent}{self.py_arg_name} : {str_types(self.types, " or ")}'] docstring.extend(list_description) else: docstring = [] return docstring - def to_py_signature(self) -> str: - """Return the Python signature of the argument.""" - if self.py_arg_name not in ["--", "–", ""]: - kwarg = f'{self.py_arg_name}: {self.str_types(" | ")}=""' - else: - kwarg = None - return kwarg - class XMLCommand(Element): """Provides the XML command from the documentation.""" @@ -2403,6 +2403,7 @@ def __init__( self._group = None self._is_archived = False self._refentry = refentry + self._max_length = 100 # parse the command super().__init__(self._refentry, parse_children=not meta_only) @@ -2512,19 +2513,30 @@ def group(self, group): """Set the group of the command.""" self._group = group - def py_signature(self, indent="") -> str: + def py_signature(self, custom_functions: CustomFunctions, indent="") -> str: """Beginning of the Python command's definition.""" args = ["self"] arg_desc = self.arg_desc - if len(arg_desc) > 0: - for argument in arg_desc: - if argument.to_py_signature() is not None: - args.append(argument.to_py_signature()) + if custom_functions is not None and ( + self.py_name in custom_functions.py_names and self.py_name in custom_functions.py_args + ): + for argument in custom_functions.py_args[self.py_name]: + new_arg = to_py_signature( + argument, [str] + ) # checks are not done for custom functions + if new_arg is not None: + args.append(new_arg) + else: + if len(arg_desc) > 0: + for argument in arg_desc: + new_arg = to_py_signature(argument.py_arg_name, argument.types) + if new_arg is not None: + args.append(new_arg) arg_sig = ", ".join(args) return f"{indent}def {self.py_name}({arg_sig}, **kwargs):" - def py_docstring(self, custom_functions, max_length=100): + def py_docstring(self, custom_functions: CustomFunctions) -> str: """Python docstring of the command.""" xml_cmd = f"{self._terms['pn006p']} Command: `{self.name} <{self.url}>`_" @@ -2539,7 +2551,7 @@ def py_docstring(self, custom_functions, max_length=100): items += [""] + textwrap.wrap("Default: " + self.default.to_rst()) if self.args is not None: items += [""] + self.py_parm( - max_length, links=self._links, base_url=self._base_url, fcache=self._fcache + custom_functions, links=self._links, base_url=self._base_url, fcache=self._fcache ) if custom_functions is not None and ( self.py_name in custom_functions.py_names @@ -2547,7 +2559,7 @@ def py_docstring(self, custom_functions, max_length=100): ): items += [""] + custom_functions.py_returns[self.py_name] if self.notes is not None: - items += [""] + self.py_notes(max_length) + items += [""] + self.py_notes(custom_functions) if custom_functions is not None and ( self.py_name in custom_functions.py_names and self.py_name in custom_functions.py_examples @@ -2763,28 +2775,39 @@ def term_replacer(match): return docstr - def py_notes(self, max_length=100): + def py_notes(self, custom_functions: CustomFunctions = None): """Python-formatted notes string.""" lines = ["Notes", "-" * 5] if self.notes.tag in item_needing_all: notes = self.notes.to_rst( + max_length=self._max_length, links=self._links, base_url=self._base_url, fcache=self._fcache, ) elif self.notes.tag in item_needing_links_base_url: - notes = self.notes.to_rst(links=self._links, base_url=self._base_url) + notes = self.notes.to_rst( + max_length=self._max_length, links=self._links, base_url=self._base_url + ) elif self.notes.tag in item_needing_fcache: - notes = self.notes.to_rst(links=self._links, fcache=self._fcache) + notes = self.notes.to_rst( + max_length=self._max_length, links=self._links, fcache=self._fcache + ) else: notes = self.notes.to_rst() if "flat-table" not in "".join(notes) and ".. code::" not in "".join(notes): - notes = resize_length(notes, 100, list=True) + notes = resize_length(notes, self._max_length, list=True) lines.extend(notes) else: lines.append(notes) + if custom_functions is not None and ( + self.py_name in custom_functions.py_names and self.py_name in custom_functions.py_notes + ): + if len("\n".join(lines)) < len("\n".join(custom_functions.py_notes[self.py_name])): + lines = custom_functions.py_notes[self.py_name] + return lines @property @@ -2828,15 +2851,37 @@ def __repr__(self): return "\n".join(lines) - def py_parm(self, max_length=100, indent="", links=None, base_url=None, fcache=None): + def py_parm(self, custom_functions=None, indent="", links=None, base_url=None, fcache=None): """Python parameter's string.""" lines = [] arg_desc = self.arg_desc + if len(arg_desc) > 0: lines.append("Parameters") + + if custom_functions is not None and ( + self.py_name in custom_functions.py_names and self.py_name in custom_functions.py_args + ): + initial_py_arg_list = [argument.py_arg_name for argument in arg_desc] + if set(custom_functions.py_args[self.py_name]) == set(initial_py_arg_list): + if len(arg_desc) > 0: + lines.append("-" * 10) + for argument in arg_desc: + lines.extend( + argument.to_py_docstring( + self._max_length, indent, links, base_url, fcache + ) + ) + lines.append("") + else: + lines.extend(custom_functions.py_params[self.py_name]) + + elif len(arg_desc) > 0: lines.append("-" * 10) for argument in arg_desc: - lines.extend(argument.to_py_docstring(max_length, indent, links, base_url, fcache)) + lines.extend( + argument.to_py_docstring(self._max_length, indent, links, base_url, fcache) + ) lines.append("") return lines @@ -2892,13 +2937,13 @@ def to_python(self, custom_functions=None, indent=""): imports = "\n".join(custom_functions.lib_import[self.py_name]) out = f""" {imports} -{self.py_signature(indent)} +{self.py_signature(custom_functions, indent)} {docstr} {self.py_source(custom_functions, indent)} """ else: out = f""" -{self.py_signature(indent)} +{self.py_signature(custom_functions, indent)} {docstr} {self.py_source(custom_functions, indent)} """ diff --git a/src/pyconverter/xml2py/cli.py b/src/pyconverter/xml2py/cli.py index 77061c64a..5f5ca5dd6 100644 --- a/src/pyconverter/xml2py/cli.py +++ b/src/pyconverter/xml2py/cli.py @@ -37,7 +37,7 @@ def create_package( target_path: Union[Path, None] = None, template_path: Union[Path, None] = None, custom_functions_path: Union[Path, None] = None, - run_black: bool = False, + run_pre_commit: bool = False, max_docstring_length: int = 100, ) -> None: """Create Python package based on a XML documentation. @@ -56,8 +56,8 @@ def create_package( custom_functions_path: str or Path, optional Path to the directory that contains the functions that need to be customized. The default value is None. - run_black: bool, optional - Whether to run black CLI on the autogenerated package source code. + run_pre_commit: bool, optional + Whether to run pre-commit hooks on the autogenerated package source code. The default value is ``False``. max_docstring_length: int, optional Maximum length of the generated docstrings. @@ -110,8 +110,8 @@ def create_package( ) package_path = target_path / "package" wr.write_docs(package_path, package_structure) - if run_black is True: - formatter.run_black(package_path, max_docstring_length) + if run_pre_commit is True: + formatter.run_pre_commit(package_path) @click.group() @@ -134,17 +134,11 @@ def version(): type=click.Path(exists=True), help="Path to the directory that contains the XML documentation to convert.", ) -@click.option( - "-f", - "--func-path", - type=click.Path(exists=True), - help="Path to the directory that contains the functions that need to be customized.", -) @click.option( "-p", "--targ-path", type=click.Path(), - help="Path to the directory where you want the autogenerated package to be created.", + help="Path where to store the autogenerated package.", ) @click.option( "-t", @@ -153,11 +147,16 @@ def version(): help="Path to the directory that contains the template to use.", ) @click.option( - "-b", - "--run-black", + "-f", + "--func-path", + type=click.Path(exists=True), + help="Path to the directory that contains the functions that need to be customized.", +) +@click.option( + "-r", + "--run-pre-commit", type=click.BOOL, - default=False, - help="Whether to run Black CLI on the autogenerated package source code. The default is 'False'.", # noqa : E501 + help="Whether to run the pre-commit hook on the autogenerated package source code.", ) @click.option( "-l", @@ -168,11 +167,11 @@ def version(): ) def package( xml_path: Path, - func_path: Path, targ_path: Path, template_path: Path, - run_black: bool, + func_path: Path, + run_pre_commit: bool, max_length: int, ) -> None: """Create a Python package from your XML documentation.""" - create_package(xml_path, targ_path, template_path, func_path, run_black, max_length) + create_package(xml_path, targ_path, template_path, func_path, run_pre_commit, max_length) diff --git a/src/pyconverter/xml2py/custom_functions.py b/src/pyconverter/xml2py/custom_functions.py index c255582e5..4c1f645c5 100644 --- a/src/pyconverter/xml2py/custom_functions.py +++ b/src/pyconverter/xml2py/custom_functions.py @@ -24,6 +24,8 @@ from pathlib import Path from typing import Tuple +import regex as re + def get_docstring_lists(filename: str) -> Tuple[list[str], list[str], list[str], list[str]]: """ @@ -49,12 +51,16 @@ def get_docstring_lists(filename: str) -> Tuple[list[str], list[str], list[str], with open(filename, "r") as pyfile: lines = pyfile.readlines() bool_def = False + bool_param = False bool_return = False - bool_examples = False bool_notes = False + bool_examples = False begin_docstring = False end_docstring = False + list_py_args = [] + list_py_params = [] list_py_returns = [] + list_py_notes = [] list_py_examples = [] list_py_code = [] list_import = [] @@ -63,27 +69,45 @@ def get_docstring_lists(filename: str) -> Tuple[list[str], list[str], list[str], list_import.append(line) elif "def" in line and bool_def is False: bool_def = True + split_def = line.split(",") + for split_arg in split_def: + if "**kwarg" in split_arg: + break + elif ":" in split_arg and "=" in split_arg: + find = re.search(r"\w*(?=\:)", split_arg).group() + list_py_args.append(find) + elif "=" in split_arg: + find = re.search(r"\w*(?=\=)", split_arg).group() + list_py_args.append(find) elif '"""' in line and begin_docstring is False: begin_docstring = True elif '"""' in line and begin_docstring is True: bool_return = False bool_examples = False end_docstring = True + elif "Parameters\n" in line: + bool_param = True + bool_return = False + bool_examples = False + bool_notes = False elif "Returns\n" in line: bool_return = True + bool_param = False bool_examples = False bool_notes = False list_py_returns.append(line.strip()) elif "Examples\n" in line: bool_examples = True + bool_param = False bool_return = False bool_notes = False list_py_examples.append(line.strip()) elif "Notes\n" in line: bool_notes = True + bool_param = False bool_return = False bool_examples = False - list_py_returns.append(line.strip()) + list_py_notes.append(line.strip()) # Section order within docstrings: Returns, Notes, Examples elif end_docstring is True: list_py_code.append(line) @@ -96,9 +120,31 @@ def get_docstring_lists(filename: str) -> Tuple[list[str], list[str], list[str], else: list_py_returns.append(4 * " " + line.strip()) elif bool_notes is True: - pass # Notes are obtained from the converter + list_py_notes.append(line.strip()) # Notes are obtained from the converter + elif bool_param is True: + no_indent = [ + "int\n", + "float\n", + "str\n", + "-------\n", + "None\n", + "bool\n", + ", optional\n", + ] + if any(n in line for n in no_indent): + list_py_params.append(line.strip()) + else: + list_py_params.append(4 * " " + line.strip()) - return list_py_returns, list_py_examples, list_py_code, list_import + return ( + list_py_args, + list_py_params, + list_py_returns, + list_py_notes, + list_py_examples, + list_py_code, + list_import, + ) # ############################################################################ @@ -112,6 +158,7 @@ class CustomFunctions: def __init__(self): self._path = "" self._py_names = [] + self._py_params = {} self._py_returns = {} self._py_examples = {} self._py_code = {} @@ -122,18 +169,33 @@ def __init__(self, path: Path) -> None: if not Path(path).is_dir(): raise (FileExistsError, f"The path_functions {path} does not exist.") self._py_names = [] + self._py_args = {} + self._py_params = {} self._py_returns = {} + self._py_notes = {} self._py_examples = {} self._py_code = {} self._lib_import = {} for filename in Path(path).glob("*.py"): py_name = filename.stem self._py_names.append(py_name) - list_py_returns, list_py_examples, list_py_code, list_import = get_docstring_lists( - filename - ) + ( + list_py_args, + list_py_params, + list_py_returns, + list_py_notes, + list_py_examples, + list_py_code, + list_import, + ) = get_docstring_lists(filename) + if len(list_py_args) > 0: + self._py_args[py_name] = list_py_args + if len(list_py_params) > 0: + self._py_params[py_name] = list_py_params if len(list_py_returns) > 0: self._py_returns[py_name] = list_py_returns + if len(list_py_notes) > 0: + self._py_notes[py_name] = list_py_notes if len(list_py_examples) > 0: self._py_examples[py_name] = list_py_examples if len(list_py_code) > 0: @@ -159,11 +221,23 @@ def path(self, path: Path) -> None: for filename in Path(path).glob("*.py"): py_name = filename.stem self._py_names.append(py_name) - list_py_returns, list_py_examples, list_py_code, list_import = get_docstring_lists( - filename - ) + ( + list_py_args, + list_py_params, + list_py_returns, + list_py_notes, + list_py_examples, + list_py_code, + list_import, + ) = get_docstring_lists(filename) + if len(list_py_args) > 0: + self._py_args[py_name] = list_py_args + if len(list_py_params) > 0: + self._py_params[py_name] = list_py_params if len(list_py_returns) > 0: self._py_returns[py_name] = list_py_returns + if len(list_py_notes) > 0: + self._py_notes[py_name] = list_py_notes if len(list_py_examples) > 0: self._py_examples[py_name] = list_py_examples if len(list_py_code) > 0: @@ -176,6 +250,16 @@ def py_names(self) -> list: """List with all customized functions located in the folder.""" return self._py_names + @property + def py_args(self) -> dict: + """Dictionary containing the python arguments if any.""" + return self._py_args + + @property + def py_params(self) -> dict: + """Dictionary containing the ``Parameters`` section if any.""" + return self._py_params + @property def py_returns(self) -> dict: """Dictionary containing the ``Returns`` section if any.""" @@ -186,6 +270,11 @@ def py_examples(self) -> dict: """Dictionary containing the ``Examples`` section if any.""" return self._py_examples + @property + def py_notes(self) -> dict: + """Dictionary containing the ``Notes`` section if any.""" + return self._py_notes + @property def py_code(self) -> dict: """Dictionary containing the customized source code.""" diff --git a/src/pyconverter/xml2py/directory_format.py b/src/pyconverter/xml2py/directory_format.py index 30adc3841..c436111eb 100644 --- a/src/pyconverter/xml2py/directory_format.py +++ b/src/pyconverter/xml2py/directory_format.py @@ -92,7 +92,9 @@ def get_paths( graph_path = path / "graphics" if not graph_path.is_dir(): print( - f"WARNING: the path {graph_path} does not exist. Follow the predefined format or enter the graphic path manually." # noqa : E501 + f"WARNING: the path {graph_path} does not exist.", + "Follow the predefined format or enter the graphic", + "path manually.", # noqa : E501 ) if link_path is None: diff --git a/src/pyconverter/xml2py/formatter.py b/src/pyconverter/xml2py/formatter.py index fe1d32880..e6561314c 100644 --- a/src/pyconverter/xml2py/formatter.py +++ b/src/pyconverter/xml2py/formatter.py @@ -26,7 +26,17 @@ import os -def run_black(package_path, max_docstring_length): - """Run `Black `_ on the autogenerated package.""" - path = os.path.join(package_path, "src/pyconverter/generatedcommands") - os.system(f"black {path} -l {max_docstring_length}") +def run_pre_commit(package_path) -> None: + """Run `pre-commit `_ on the autogenerated package.""" + output = 1 + cur_run = 0 + max_run = 10 + while cur_run < max_run and output != 0: + cur_run += 1 + output = os.system( + f"pre-commit run --all-files --config {package_path}/.pre-commit-config.yaml" + ) + if output != 0: + raise RuntimeError("Pre-commit failed.") + else: + print("Pre-commit ran successfully.") diff --git a/src/pyconverter/xml2py/writer.py b/src/pyconverter/xml2py/writer.py index 823929cf1..f95a00405 100644 --- a/src/pyconverter/xml2py/writer.py +++ b/src/pyconverter/xml2py/writer.py @@ -49,9 +49,6 @@ '``"``': "``", } -# XML commands to skip -SKIP_XML = {"*IF", "*ELSE", "*RETURN", "*DEL"} # Equivalent to if, else, return, del - def convert(directory_path): """ @@ -213,7 +210,7 @@ def copy_template_package(template_path: Path, new_package_path: Path, clean: bo shutil.copy(filename, new_package_path) -def write_global__init__file(library_path: Path) -> None: +def write_global__init__file(library_path: Path, config_path: Path) -> None: """ Write the ``__init__.py`` file for the package generated. @@ -222,10 +219,22 @@ def write_global__init__file(library_path: Path) -> None: library_path: Path Path object of the directory containing the generated package. """ - mod_file = library_path / "__init__.py" - with open(mod_file, "w") as fid: - fid.write(f"from . import (\n") + subfolder_values = get_config_data_value(config_path, "subfolders") + + if subfolder_values: + init_folder = library_path + for subfolder in subfolder_values: + init_folder = init_folder.parent + initial_imports = ".".join(subfolder_values) + else: + init_folder = library_path + initial_imports = "" + + init_path = init_folder / "__init__.py" + + with open(init_path, "w") as fid: + fid.write(f"from .{initial_imports} import (\n") for dir in library_path.iterdir(): if dir.is_dir(): fid.write(f" {dir.stem},\n") @@ -262,7 +271,7 @@ def write__init__file(library_path: Path) -> None: fid.close() -def get_library_path(new_package_path: Path, config_path: Path) -> Path: +def get_library_path(new_package_path: Path, config_path: Path, subfolder: bool = True) -> Path: """ Get the desired library path with the following format: ``new_package_path/library_structure``. @@ -286,6 +295,10 @@ def get_library_path(new_package_path: Path, config_path: Path) -> Path: library_name = get_config_data_value(config_path, "library_name_structured") if not "src" in library_name: library_name.insert(0, "src") + if subfolder: + subfolder_values = get_config_data_value(config_path, "subfolders") + if subfolder_values: + library_name.extend(subfolder_values) return new_package_path.joinpath(*library_name) @@ -408,6 +421,8 @@ def write_source( logging.info(f"Creating package {new_package_name}...") new_package_path = target_path / new_package_name + ignored_commands = set(get_config_data_value(config_path, "ignored_commands")) + if clean: if new_package_path.is_dir(): shutil.rmtree(new_package_path) @@ -420,7 +435,7 @@ def write_source( if structured == False: package_structure = None for initial_command_name, command_obj in tqdm(command_map.items(), desc="Writing commands"): - if initial_command_name in SKIP_XML: + if initial_command_name in ignored_commands: continue python_name = name_map[initial_command_name] path = library_path / f"{python_name}.py" @@ -439,7 +454,7 @@ def write_source( all_commands = [] specific_classes = get_config_data_value(config_path, "specific_classes") for command in tqdm(command_map.values(), desc="Writing commands"): - if command.name in SKIP_XML or command.group is None: + if command.name in ignored_commands or command.group is None: continue module_name, initial_class_name, module_path = get_module_info(library_path, command) @@ -499,7 +514,7 @@ def write_source( f"Failed to execute '{python_method}' from '{file_path}'." ) from e - write_global__init__file(library_path) + write_global__init__file(library_path, config_path) write__init__file(library_path) logging.info(f"Commands written to {library_path}") @@ -534,6 +549,9 @@ def write_docs( library_name = get_config_data_value(config_path, "library_name_structured") if library_name[0] == "src": library_name.pop(0) + subfolder_values = get_config_data_value(config_path, "subfolders") + if subfolder_values: + library_name.extend(subfolder_values) library_name = ".".join(library_name) doc_package_path = package_path / "doc" / "source" diff --git a/tests/conftest.py b/tests/conftest.py index 534cea092..6557a2242 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -132,7 +132,11 @@ def config_path(cwd): @pytest.fixture def library_name_structured(config_path): - return get_config_data_value(config_path, "library_name_structured") + lib_structure = get_config_data_value(config_path, "library_name_structured") + subfolders = get_config_data_value(config_path, "subfolders") + if subfolders is not None: + lib_structure.extend(get_config_data_value(config_path, "subfolders")) + return lib_structure @pytest.fixture diff --git a/tests/customized_functions/inquire.py b/tests/customized_functions/inquire.py new file mode 100644 index 000000000..e098367f2 --- /dev/null +++ b/tests/customized_functions/inquire.py @@ -0,0 +1,180 @@ +# Copyright (C) 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# 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: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# 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. + + +def inquire(self, strarray="", func="", arg1="", arg2=""): + """Returns system information. + + By default, with no arguments, it returns the working directory. + + >>> mapdl.inquire() + C:\\Users\\user\\AppData\\Local\\Temp\\ansys_nynvxsaooh + + Parameters + ---------- + strarray : str, optional + Name of the string array or parameter that will hold the returned values. + Normally, if used in a python script you should just work with the + return value from this method. + + func : str, optional + Specifies the type of system information returned. See the + notes section for more information. + + arg1 : str, optional + First argument. See notes for ``arg1`` definition. + + arg2 : str, optional + Second argument. See notes for ``arg1`` definition. + + Returns + ------- + str + Value of the inquired item. + + Notes + ----- + The ``/INQUIRE`` command is valid in any processor. + + .. warning:: + Take note that from version 0.60.4 and later, the command behaviour + has been changed. + Previously, the ``StrArray`` argument was omitted. For example: + >>> mapdl.inquire('DIRECTORY') + C:\\Users\\user\\AppData\\Local\\Temp\\ansys_nynvxsaooh + + Now this will raise an exception. + The default behaviour now, requires to input ``StrArray``: + >>> mapdl.inquire('', 'DIRECTORY') + C:\\Users\\user\\AppData\\Local\\Temp\\ansys_nynvxsaooh + + **GENERAL FUNC OPTIONS** + + - ``LOGIN`` - Returns the pathname of the login directory on Linux + systems or the pathname of the default directory (including + drive letter) on Windows systems. + - ``DOCU`` - Pathname of the ANSYS documentation directory. + - ``APDL`` - Pathname of the ANSYS APDL directory. + - ``PROG`` - Pathname of the ANSYS executable directory. + - ``AUTH`` - Pathname of the directory in which the license file resides. + - ``USER`` - Name of the user currently logged-in. + - ``DIRECTORY`` - Pathname of the current directory. + - ``JOBNAME`` - Current Jobname. + - ``RSTDIR`` - Result file directory. + - ``RSTFILE`` - Result file name. + - ``RSTEXT`` - Result file extension. + - ``OUTPUT`` - Current output file name. + + + **RETURNING THE VALUE OF AN ENVIRONMENT VARIABLE TO A PARAMETER** + + If ``FUNC=ENV``, the command format is ``/INQUIRE,StrArray,ENV,ENVNAME,Substring``. + In this instance, ENV specifies that the command should return the + value of an environment variable. + The following defines the remaining fields: + + Envname: + Specifies the name of the environment variable. + + Substring: + If ``Substring = 1``, the first substring (up to the first colon (:)) is returned. + If ``Substring = 2``, the second substring is returned, etc. For Windows platforms, + the separating character is semicolon (;). + If this argument is either blank or 0, the entire value of the environment + variable is returned. + + + **RETURNING THE VALUE OF A TITLE TO A PARAMETER** + + If ``FUNC = TITLE``, the command format is ``/INQUIRE,StrArray,TITLE,Title_num``. + In this context, the value of Title_num can be blank or ``1`` through ``5``. If the + value is ``1`` or blank, the title is returned. If the value is ``2`` through ``5``, + a corresponding subtitle is returned (``2`` denoting the first subtitle, and so on). + + + **RETURNING INFORMATION ABOUT A FILE TO A PARAMETER** + + The ``/INQUIRE`` command can also return information about specified files + within the file system. + For these capabilities, the format is ``/INQUIRE,Parameter,FUNC,Fname, Ext, --``. + The following defines the fields: + + Parameter: + Name of the parameter that will hold the returned values. + + Func: + Specifies the type of file information returned: + + EXIST: + Returns a ``1`` if the specified file exists, and ``0`` if it does not. + + DATE: + Returns the date stamp of the specified file in the format ``*yyyymmdd.hhmmss*``. + + SIZE: + Returns the size of the specified file in MB. + + WRITE: + Returns the status of the write attribute. A ``0`` denotes no write permission while a ``1`` denotes + write permission. + + READ: + Returns the status of the read attribute. A ``0`` denotes no read permission while a ``1`` denotes read + permission. + + EXEC: + Returns the status of the execute attribute (this has meaning only on Linux). A ``0`` denotes no + execute permission while a ``1`` denotes execute permission. + + LINES: + Returns the number of lines in an ASCII file. + + Fname: + File name and directory path (248 characters maximum, including the characters needed for the + directory path). An unspecified directory path defaults to the working directory; in this case, you + can use all 248 characters for the file name. + + Ext: + Filename extension (eight-character maximum). + + Examples + -------- + Return the MAPDL working directory + >>> mapdl.inquire('', 'DIRECTORY') + C:\\Users\\gayuso\\AppData\\Local\\Temp\\ansys_nynvxsaooh + + Or + + >>> mapdl.inquire() + C:\\Users\\gayuso\\AppData\\Local\\Temp\\ansys_nynvxsaooh + + Return the job name + + >>> mapdl.inquire('', 'JOBNAME') + file + + Return the result file name + + >>> mapdl.inquire('', 'RSTFILE') + 'file.rst' + """ + return self.run(f"/INQUIRE,{strarray},{func},{arg1},{arg2}") diff --git a/tests/test_cli.py b/tests/test_cli.py index 8d95fdf13..e585338bc 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -45,8 +45,8 @@ def test_cli_main_package_group(): assert "Create a Python package from your XML documentation." in result.output assert "-x, --xml-path PATH" in result.output - assert "-f, --func-path PATH" in result.output assert "-p, --targ-path PATH" in result.output assert "-t, --template-path PATH" in result.output - assert "-b, --run-black BOOLEAN" in result.output + assert "-f, --func-path PATH" in result.output + assert "-r, --run-pre-commit BOOLEAN" in result.output assert "-l, --max-length INTEGER" in result.output diff --git a/tests/test_custom_functions.py b/tests/test_custom_functions.py index 7ca82bfde..5bec7b4a0 100644 --- a/tests/test_custom_functions.py +++ b/tests/test_custom_functions.py @@ -34,11 +34,15 @@ def test_customfunctions(custom_functions): def test_get_docstring_lists(path_custom_functions): path_custom_function = path_custom_functions / "kdist.py" ( + list_py_args, + list_py_params, list_py_returns, + list_py_notes, list_py_examples, list_py_code, list_import, ) = cf.get_docstring_lists(path_custom_function) + "kp1" in list_py_args " list" in list_py_returns "Compute the distance between two keypoints." in list_py_examples 'return parse.parse_kdist(self.run(f"KDIST,{kp1},{kp2}", **kwargs))' in list_py_code diff --git a/tests/test_writer.py b/tests/test_writer.py index e152eac22..1ac6f380e 100644 --- a/tests/test_writer.py +++ b/tests/test_writer.py @@ -36,7 +36,7 @@ def test_convert(command_map, custom_functions): command_map["E"].py_source(custom_functions) == ' command = f"E,{i},{j},{k},{l},{m},{n},{o},{p}"\n return self.run(command, **kwargs)\n' # noqa : E501 ) - assert 'def zoom(self, wn: str="", lab: str="", x1: str="", y1: str="", x2: str="", y2: str="", **kwargs):\n r"""Zooms a region of a display window.\n\n' in command_map[ # noqa : E501 + assert 'def zoom(self, wn: str = "", lab: str = "", x1: str = "", y1: str = "", x2: str = "", y2: str = "", **kwargs):\n r"""Zooms a region of a display window.\n\n' in command_map[ # noqa : E501 "/ZOOM" ].to_python( custom_functions