diff --git a/doc/source/abstractions/app-interface.rst b/doc/source/abstractions/app-interface.rst index d5a71874..84bc5f4c 100644 --- a/doc/source/abstractions/app-interface.rst +++ b/doc/source/abstractions/app-interface.rst @@ -61,45 +61,49 @@ within this method. .. code:: python - def create_open_region(self, frequency="1GHz", boundary="Radiation", - apply_infinite_gp=False, gp_axis="-z"): - """Create an open region on the active editor. - - Parameters - ---------- - frequency : str, optional - Frequency with units. The default is ``"1GHz"``. - boundary : str, optional - Type of the boundary. The default is ``"Radiation"``. - apply_infinite_gp : bool, optional - Whether to apply an infinite ground plane. The default is ``False``. - gp_axis : str, optional - The default is ``"-z"``. - - Returns - ------- - bool - ``True`` when successful, ``False`` when failed. - - Examples - -------- - Create an open region in the active editor at 1 GHz. - - >>> hfss.create_open_region(frequency="1GHz") - - """ - vars = [ - "NAME:Settings", - "OpFreq:=", frequency, - "Boundary:=", boundary, - "ApplyInfiniteGP:=", apply_infinite_gp - ] - if apply_infinite_gp: - vars.append("Direction:=") - vars.append(gp_axis) - - self._omodelsetup.CreateOpenRegion(vars) - return True + def create_open_region( + self, frequency="1GHz", boundary="Radiation", apply_infinite_gp=False, gp_axis="-z" + ): + """Create an open region in the active editor. + + Parameters + ---------- + frequency : str, optional + Frequency with units. The default is ``"1GHz"``. + boundary : str, optional + Type of the boundary. The default is ``"Radiation"``. + apply_infinite_gp : bool, optional + Whether to apply an infinite ground plane. The default is ``False``. + gp_axis : str, optional + The default is ``"-z"``. + + Returns + ------- + bool + ``True`` when successful, ``False`` when failed. + + Examples + -------- + Create an open region in the active editor at 1 GHz. + + >>> hfss.create_open_region(frequency="1GHz") + + """ + vars = [ + "NAME:Settings", + "OpFreq:=", + frequency, + "Boundary:=", + boundary, + "ApplyInfiniteGP:=", + apply_infinite_gp, + ] + if apply_infinite_gp: + vars.append("Direction:=") + vars.append(gp_axis) + + self._omodelsetup.CreateOpenRegion(vars) + return True Here, the COM ``CreateOpenRegion`` method is abstracted, encapsulating the model setup object. There's no reason why a user needs direct diff --git a/doc/source/coding-style/code/tox-flit.rst b/doc/source/coding-style/code/tox-flit.rst new file mode 100644 index 00000000..ef8bf454 --- /dev/null +++ b/doc/source/coding-style/code/tox-flit.rst @@ -0,0 +1,43 @@ +.. code-block:: ini + + [tox] + description = Default tox environments list + envlist = + style,{py37,py38,py39,py310}{,-coverage},doc + skip_missing_interpreters = true + isolated_build = true + isolated_build_env = build + + [testenv] + description = Checks for project unit tests and coverage (if desired) + basepython = + py37: python3.7 + py38: python3.8 + py39: python3.9 + py310: python3.10 + py: python3 + {style,reformat,doc,build}: python3 + setenv = + PYTHONUNBUFFERED = yes + coverage: PYTEST_EXTRA_ARGS = --cov=ansys.product --cov-report=term --cov-report=xml --cov-report=html + deps = + -r{toxinidir}/requirements/requirements_tests.txt + commands = + pytest {env:PYTEST_MARKERS:} {env:PYTEST_EXTRA_ARGS:} {posargs:-vv} + + [testenv:style] + description = Checks project code style + skip_install = true + deps = + pre-commit + commands = + pre-commit install + pre-commit run --all-files --show-diff-on-failure + + [testenv:doc] + description = Check if documentation generates properly + deps = + -r{toxinidir}/requirements/requirements_doc.txt + commands = + sphinx-build -d "{toxworkdir}/doc_doctree" doc/source "{toxworkdir}/doc_out" --color -vW -bhtml + diff --git a/doc/source/coding-style/code/tox-poetry.rst b/doc/source/coding-style/code/tox-poetry.rst new file mode 100644 index 00000000..c44d4e65 --- /dev/null +++ b/doc/source/coding-style/code/tox-poetry.rst @@ -0,0 +1,46 @@ +.. code-block:: ini + + [tox] + description = Default tox environments list + envlist = + style,{py37,py38,py39,py310}{,-coverage},doc + skip_missing_interpreters = true + isolated_build = true + + [testenv] + description = Checks for project unit tests and coverage (if desired) + basepython = + py37: python3.7 + py38: python3.8 + py39: python3.9 + py310: python3.10 + py: python3 + {style,reformat,doc,build}: python3 + skip_install = true + whitelist_externals = + poetry + setenv = + PYTHONUNBUFFERED = yes + coverage: PYTEST_EXTRA_ARGS = --cov=ansys.product --cov-report=term --cov-report=xml --cov-report=html + deps = + -r{toxinidir}/requirements/requirements_tests.txt + commands = + poetry install + poetry run pytest {env:PYTEST_MARKERS:} {env:PYTEST_EXTRA_ARGS:} {posargs:-vv} + + [testenv:style] + description = Checks project code style + skip_install = true + deps = + pre-commit + commands = + pre-commit install + pre-commit run --all-files --show-diff-on-failure + + [testenv:doc] + description = Check if documentation generates properly + deps = + -r{toxinidir}/requirements/requirements_doc.txt + commands = + poetry run sphinx-build -d "{toxworkdir}/doc_doctree" doc/source "{toxworkdir}/doc_out" --color -vW -bhtml + diff --git a/doc/source/coding-style/formatting-tools.rst b/doc/source/coding-style/formatting-tools.rst new file mode 100644 index 00000000..2227226e --- /dev/null +++ b/doc/source/coding-style/formatting-tools.rst @@ -0,0 +1,253 @@ +Code Style Tools +================ + +There are many tools for checking code style. This section presents some of +the most popular ones in the Python ecosystem. A minimum configuration is +provided for each one so that you can easily include them in your PyAnsys project. + +Most of the tools presented can be configured using :ref:`the +\`\`pyproject.toml\`\` file`. Avoiding dotfiles leads to a much +cleaner root project directory. + + +Black +----- +`Black`_ is the most popular code formatter in the Python community because it is +maintained by the Python Software Foundation. It allows for a minimum +configuration to ensure that the Python code format looks almost the same across +projects. + +While `PEP 8`_ imposes a default line length of 79 characters, `black`_ has +a default line length of 88 characters. + +The minimum `black`_ configuration for a PyAnsys project should look like this: + +.. code-block:: toml + + [tool.black] + line-length: "" + + +Isort +----- +The goal of `isort`_ is to properly format ``import`` statements by making sure +that they follow the standard order: library, third-party libraries, and custom libraries. + +When using `isort`_ with `black`_, it is important to properly configure both +tools so that no conflicts arise. To accomplish this, use the +``--porfile black`` flag in `isort`_. + +.. code-block:: toml + + [tool.isort] + profile = "black" + force_sort_within_sections = true + line_length = "" + default_section = "THIRDPARTY" + src_paths = ["doc", "src", "tests"] + + +Flake8 +------ +The goal of `flake8` is to act as a `PEP 8`_ compliance checker. Again, if +this tool is being used with `black`_, it is important to make sure that no +conflicts arise. + +The following configuration is the minimum one to set up `flake8`_ together with +`black`_. + +The configuration for `flake8`_ must be specified in a ``.flake8`` file. + +.. code-block:: toml + + [flake8] + max-line-length = 88 + extend-ignore = E203 + +Flake8 has many options that can be set within the configuration file. +For more information, see this `Flake8 documentation topic +`__. + +The example configuration defines these options: + +- ``exclude`` + Subdirectories and files to exclude from the check. + +- ``select`` + Sequence of error codes that Flake8 is to report errors + for. The set in the above configuration is a basic set of errors to + check for and is not an exhaustive list. + + For a full list of error codes and their descriptions, see this `Flake8 + documentation topic `__. + +- ``count`` + Total number of errors to print at the end of the check. + +- ``max-complexity`` + Maximum allowed McCabe complexity value for a block of code. + The value of 10 was chosen because it is a common default. + +- ``statistics`` + Number of occurrences of each error or warning code + to print as a report at the end of the check. + + +Code Coverage +------------- +Code coverage indicates the percentage of the codebase tested by the test +suite. Code coverage should be as high as possible to guarantee that every piece +of code has been tested. + +For ``PyAnsys``, code coverage is done using `pytest-cov`_, a `pytest`_ plugin +that triggers the code coverage analysis once your test suite has executed. + +Considering the layout presented in :ref:`Required Files`, the following +configuration for code coverage is the minimum one required for a ``PyAnsys`` +project: + +.. code-block:: toml + + [tool.coverage.run] + source = ["ansys."] + + [tool.coverage.report] + show_missing = true + +Pre-commit +---------- +To ensure that every commit you make is compliant with the code style +guidelines for PyAnsys, you can take advantage of `pre-commit`_ in your project. +Every time you stage some changes and try to commit them, `pre-commit`_ only +allows them to be committed if all defined hooks succeed. + +The configuration for `pre-commit`_ must be defined in a +``.pre-commit-config.yaml`` file. The following lines present a minimum +`pre-commit`_ configuration that includes both code and documentation +formatting tools. + + +.. code-block:: yaml + + repos: + + - repo: https://github.com/psf/black + rev: X.Y.Z + hooks: + - id: black + + - repo: https://github.com/pycqa/isort + rev: X.Y.Z + hooks: + - id: isort + + - repo: https://github.com/PyCQA/flake8 + rev: X.Y.Z + hooks: + - id: flake8 + + - repo: https://github.com/codespell-project/codespell + rev: vX.Y.Z + hooks: + - id: codespell + + - repo: https://github.com/pycqa/pydocstyle + rev: X.Y.Z + hooks: + - id: pydocstyle + additional_dependencies: [toml] + exclude: "tests/" + +Installing ``pre-commit`` +~~~~~~~~~~~~~~~~~~~~~~~~~ +You can install ``pre-commit`` by running: + +.. code-block:: bash + + python -m pip install pre-commit + +Then, ensure that you install it as a ``Git hook`` by running: + +.. code-block:: bash + + pre-commit install + +Using ``pre-commit`` +~~~~~~~~~~~~~~~~~~~~ +One installed as described, ``pre-commit`` automatically triggers every time +that you try to commit a change. If any hook defined in `.pre-commit-config.yaml` +fails, you must fix the failing files, stage the new changes, and try to commit +them again. + +If you want to manually run ``pre-commit``, you can run: + +.. code-block:: bash + + pre-commit run --all-files --show-diff-on-failure + +This command will show the current and expected style of the code if any of +the hooks fail. + +Tox +--- +You might consider using `tox`_ in your project. While this automation +tool is similar to `Make`_, it supports testing of your package in a temporary +virtual environment. Being able to test your package in isolation rather than in +"local" mode guarantees reproducible builds. + +Configuration for `tox`_ is stored in a ``tox.ini`` file. The minimum +configuration for a PyAnsys ``py-`` project should be: + + +.. tabs:: + + .. tab:: Tox with Flit + + .. include:: code/tox-flit.rst + + .. tab:: Tox with Poetry + + .. include:: code/tox-poetry.rst + + +This minimum configuration assumes that you have a ``requirements/`` directory that +contains ``requirements_tests.txt`` and ``requirements_doc.txt``. In +addition, the ``style`` environment must execute ``pre-commit``, which guarantees +the usage of this tool in your project. + +Installing ``tox`` +~~~~~~~~~~~~~~~~~~ +You can install ``tox`` like any other Python package: + +.. code-block:: bash + + python -m pip install tox + + +Using ``tox`` +~~~~~~~~~~~~~ + +`tox`_ uses ``environments``, which are similar to ``Makefile`` rules, +to make it highly customizable. Descriptions follow of some of the most +widely used environments: + +- ``tox -e style`` checks the code style of your project. +- ``tox -e py`` runs your test suite. +- ``tox -e doc`` builds the documentation of your project. + +It is possible to run multiple environments by separating them with commas ``tox +-e ,,...```. To run all available environments, simply +run ``tox``. + + +.. LINKS AND REFERENCES + +.. _black: https://black.readthedocs.io/en/latest/ +.. _isort: https://pycqa.github.io/isort/ +.. _flake8: https://flake8.pycqa.org/en/latest/ +.. _pre-commit: https://pre-commit.com/ +.. _pytest: https://docs.pytest.org/en/latest/ +.. _pytest-cov: https://pytest-cov.readthedocs.io/en/latest/ +.. _tox: https://tox.wiki/en/latest/ +.. _PEP 8: https://www.python.org/dev/peps/pep-0008/ +.. _make: https://www.gnu.org/software/make/ diff --git a/doc/source/coding-style/index.rst b/doc/source/coding-style/index.rst new file mode 100644 index 00000000..3a9e71ce --- /dev/null +++ b/doc/source/coding-style/index.rst @@ -0,0 +1,31 @@ +Coding Style +############ + +Coding style refers to the different rules defined in a software project that +must be followed when writing source code. These rules ensure that all +the source code will look the same across the different files of the +project. + +Because the PyAnsys ecosystem consists of many projects, coding style rules +are critical to ensuring that the source code will look the same across different +projects. + +PyAnsys libraries are expected to follow `PEP 8`_ and be consistent in style and +formatting with the 'big three' data science libraries: `NumPy`_, `SciPy`_, and +`pandas`_. + +.. toctree:: + :hidden: + :maxdepth: 3 + + pep8 + formatting-tools + required-standard + + +.. LINKS AND REFERENCES + +.. _NumPy: https://numpy.org/ +.. _SciPy: https://www.scipy.org/ +.. _pandas: https://pandas.pydata.org/ +.. _PEP 8: https://www.python.org/dev/peps/pep-0008/ diff --git a/doc/source/coding-style/pep8.rst b/doc/source/coding-style/pep8.rst new file mode 100644 index 00000000..f2a2ca44 --- /dev/null +++ b/doc/source/coding-style/pep8.rst @@ -0,0 +1,813 @@ +PEP 8 +===== + +This section summarizes the key points from `PEP 8`_ and how they apply to PyAnsys +libraries. `PEP 8`_ style guideline were devised by the Python community +to increase the readability of Python code. `PEP 8`_ has been adopted by some of +the most popular libraries within the Python ecosystem, including: `NumPy`_, +`SciPy`_, and `pandas`_. + +.. _PEP 8: https://www.python.org/dev/peps/pep-0008/ +.. _NumPy: https://numpy.org/ +.. _SciPy: https://www.scipy.org/ +.. _pandas: https://pandas.pydata.org/ + + +Imports +------- +Code style guidelines follow for ``import`` statements. + +Import Location +~~~~~~~~~~~~~~~ +Imports should always be placed at the top of the file, just after any +module comments and docstrings and before module globals and +constants. This reduces the likelihood of an `ImportError`_ that +might only be discovered during runtime. + +.. _ImportError: https://docs.python.org/3/library/exceptions.html#ImportError + +.. tabs:: + + .. tab:: Avoid + + .. code-block:: python + + def compute_logbase8(x): + import math + + return math.log(8, x) + + .. tab:: Use + + .. code-block:: python + + import math + + + def compute_logbase8(x): + return math.log(8, x) + + +Imports Order +~~~~~~~~~~~~~ +For better readability, group imports in this order: + +#. Standard library imports +#. Related third-party imports +#. Local application-specific or library-specific imports + +All imports within each import grouping should be performed in alphabetical order. + +.. tabs:: + + .. tab:: Avoid + + .. code-block:: python + + import sys + import subprocess + from mypackage import mymodule + import math + + + def compute_logbase8(x): + return math.log(8, x) + + + .. tab:: Use + + .. code-block:: python + + import math + import subprocess + import sys + + from mypackage import mymodule + + + def compute_logbase8(x): + return math.log(8, x) + + +Multiple Imports +~~~~~~~~~~~~~~~~ +You should place imports in separate lines unless they are modules from the same +package. + +.. tabs:: + + .. tab:: Avoid + + .. code-block:: python + + import math, sys + + from my_package import my_module + from my_package import my_other_module + + + def compute_logbase8(x): + return math.log(8, x) + + .. tab:: Use + + .. code-block:: python + + import math + import sys + + from my_package import my_module, my_other_module + + + def compute_logbase8(x): + return math.log(8, x) + + +Import Namespaces +~~~~~~~~~~~~~~~~~ +You should avoid using wildcards in imports because doing so can make it +difficult to detect undefined names. For more information, see `Python +Anti-Patterns: using wildcard imports +<(https://docs.quantifiedcode.com/python-anti-patterns/maintainability/from_module_import_all_used.html>`_. + +.. tabs:: + + .. tab:: Avoid + + .. code-block:: python + + from my_package.my_module import * + + .. tab:: Use + + .. code-block:: python + + from my_package.my_module import myclass + + +Naming Conventions +------------------ +To achieve readable and maintainable code, use concise and descriptive names for classes, +methods, functions, and constants. Regardless of the programming language, you must follow these +global rules to determine the correct names: + +#. Choose descriptive and unambiguous names. +#. Make meaningful distinctions. +#. Use pronounceable names. +#. Use searchable names. +#. Replace magic numbers with named constants. +#. Avoid encodings. Do not append prefixes or type information. + + +Variables +~~~~~~~~~ +Do not use the characters ``'l'``, ``'O'`` , or ``'I'`` as single-character +variable names. In some fonts, these characters are indistinguishable from the +numerals one and zero. + + +Packages and Modules +~~~~~~~~~~~~~~~~~~~~ +Use a short, lowercase word or words for module names. Separate words +with underscores to improve readability. For example, use ``module.py`` +or ``my_module.py``. + +For a package name, use a short, lowercase word or words. Avoid +underscores as these must be represented as dashes when installing +from PyPi. + +.. code:: + + python -m pip install package + + +Classes +~~~~~~~ +Use `camel case `_ when naming +classes. Do not separate words with underscores. + +.. code:: python + + class MyClass: + """Docstring for MyClass""" + + ... + + +Use a lowercase word or words for Python functions or methods. Separate words +with underscores to improve readability. When naming class methods, the +following conventions apply: + +- Enclose only `dunder methods`_ with double underscores. +- Start a method that is to be considered private with double underscores. +- Start a method that is to be considered protected with a single underscore. + +.. _dunder methods: https://docs.python.org/3/reference/datamodel.html#special-method-names + +.. code:: python + + class MyClass: + """Docstring for MyClass.""" + + def __init__(self, value): + """Constructor. + + Methods with double underscores on either side are called + "dunder" methods and are special Python methods. + + """ + self._value = value + + def __private_method(self): + """This method can only be called from ``MyClass``.""" + self._value = 0 + + def _protected_method(self): + """This method should only be called from ``MyClass``. + + Protected methods can be called from inherited classes, + unlike private methods, which names are 'mangled' to avoid + these methods from being called from inherited classes. + + """ + # note how we can call private methods here + self.__private_method() + + def public_method(self): + """This method can be called external to this class.""" + self._value += 2 + + +.. note:: + + Remember that these are only conventions for naming functions and methods. In Python + there are no private or protected members, meaning that you can always access even + those members that start with underscores. + +Variables +~~~~~~~~~ +Use a lowercase single letter, word, or words when naming variables. Separate +words with underscores to improve readability. + +.. code:: python + + my_variable = 5 + +Constants are variables that are set at the module level and are used by one or +more methods within that module. Use an uppercase word or words for constants. +Separate words with underscores to improve readability. + +.. code:: python + + PI = 3.141592653589793 + CONSTANT = 4 + MY_CONSTANT = 8 + MY_OTHER_CONSTANT = 1000 + +Indentation and Line Breaks +--------------------------- +Proper and consistent indentation is important to producing +easy-to-read and maintainable code. In Python, use four spaces per +indentation level and avoid tabs. + +Indentation should be used to emphasize: + + - Body of a control statement, such as a loop or a select statement + - Body of a conditional statement + - New scope block + +.. code:: python + + class MyFirstClass: + """MyFirstClass docstring.""" + + + class MySecondClass: + """MySecondClass docstring.""" + + + def top_level_function(): + """Top level function docstring.""" + return + +For improved readability, add blank lines or wrapping lines. Two +blank lines should be added before and after all class and function +definitions. + +Inside a class, use a single line before any method definition. + +.. code-block:: python + + class MyClass: + """MyClass docstring.""" + + def first_method(self): + """First method docstring.""" + return + + def second_method(self): + """Second method docstring.""" + return + +To make it clear when a 'paragraph' of code is complete and a new section +is starting, use a blank line to separate logical sections. + +Instead of: + +.. tabs:: + + .. tab:: Avoid + + .. code-block:: python + + if x < y: + + ... + + else: + + if x > y: + + ... + + else: + + ... + + if x > 0 and x < 10: + + print("x is a positive single digit.") + + .. tab:: Use + + .. code-block:: python + + if x < y: + ... + else: + if x > y: + ... + else: + ... + + if x > 0 and x < 10: + print("x is a positive single digit.") + elif x < 0: + print("x is less than zero.") + + +Maximum Line Length +------------------- +For source code lines, best practice is to keep the length at or below +100 characters. For docstrings and comments, best practice is to keep +the length at or below 72 characters. + +Lines longer than these recommended limits might not display properly +on some terminals and tools or might be difficult to follow. For example, +this line is difficult to follow: + + +.. tabs:: + + .. tab:: Avoid + + .. code:: python + + employee_hours = [schedule.earliest_hour for employee in self.public_employees for schedule in employee.schedules] + + .. tab:: Use + + .. code-block:: python + + employee_hours = [ + schedule.earliest_hour + for employee in self.public_employees + for schedule in employee.schedules + ] + +Alternatively, instead of writing a list comprehension, you can use a +classic loop. + +Notice that sometimes it will not be possible to keep the line length below the +desired value without breaking the syntax rules. + +Comments +-------- +Because a PyAnsys library generally involves multiple physics domains, +users reading its source code do not have the same background as +the developers who wrote it. This is why it is important for a library +to have well commented and documented source code. Comments that +contradict the code are worse than no comments. Always make a priority +of keeping comments up to date with the code. + +Comments should be complete sentences. The first word should be +capitalized, unless it is an identifier that begins with a lowercase +letter. + +Here are general guidelines for writing comments: + +#. Always try to explain yourself in code by making it + self-documenting with clear variable names. +#. Don't be redundant. +#. Don't add obvious noise. +#. Don't use closing brace comments. +#. Don't comment out code that is unused. Remove it. +#. Use explanations of intent. +#. Clarify the code. +#. Warn of consequences. + +Obvious portions of the source code should not be commented. +For example, the following comment is not needed: + +.. code:: python + + # increment the counter + i += 1 + +However, an important portion of the behavior that is not self-apparent +should include a note from the developer writing it. Otherwise, +future developers may remove what they see as unnecessary. + +.. code:: python + + # Be sure to reset the object's cache prior to exporting. Otherwise, + # some portions of the database in memory will not be written. + obj.update_cache() + obj.write(filename) + + +Inline Comments +~~~~~~~~~~~~~~~ +Use inline comments sparingly. An inline comment is a comment on the +same line as a statement. + +Inline comments should be separated by two spaces from the statement. + +.. code:: python + + x = 5 # This is an inline comment + +Inline comments that state the obvious are distracting and should be +avoided: + +.. code:: python + + x = x + 1 # Increment x + + +Focus on writing self-documenting code and using short but +descriptive variable names. + +.. tabs:: + + .. tab:: Avoid + + .. code:: python + + x = "John Smith" # Student Name + + .. tab:: Use + + .. code:: python + + user_name = "John Smith" + + +Docstring Conventions +~~~~~~~~~~~~~~~~~~~~~ +A docstring is a string literal that occurs as the first statement in +a module, function, class, or method definition. A docstring becomes +the doc special attribute of the object. + +Write docstrings for all public modules, functions, classes, and +methods. Docstrings are not necessary for private methods, but such +methods should have comments that describe what they do. + +To create a docstring, surround the comments with three double quotes +on either side. + +For a one-line docstring, keep both the starting and ending ``"""`` on the +same line: + +.. code:: python + + """This is a docstring.""" + +For a multi-line docstring, put the ending ``"""`` on a line by itself. + +For more information on docstrings for PyAnsys libraries, see +:ref:`Documentation Style`. + + +Programming Recommendations +--------------------------- +The following sections provide some `PEP8 +`_ suggestions for removing +ambiguity and preserving consistency. They also address some common pitfalls +when writing Python code. + + +Booleans and Comparisons +~~~~~~~~~~~~~~~~~~~~~~~~ +Don't compare Boolean values to ``True`` or ``False`` using the +equivalence operator. + +.. tabs:: + + .. tab:: Avoid + + .. code-block:: python + + if my_bool == True: + return result + + .. tab:: Use + + .. code-block:: python + + if my_bool: + return result + +Knowing that empty sequences are evaluated to ``False``, don't compare the +length of these objects but rather consider how they would evaluate +by using ``bool()``. + +.. tabs:: + + .. tab:: Avoid + + .. code-block:: python + + my_list = [] + if not len(my_list): + raise ValueError('List is empty') + + .. tab:: Use + + .. code-block:: python + + my_list = [] + if not my_list: + raise ValueError('List is empty') + + +In ``if`` statements, use ``is not`` rather than ``not ...``. + +.. tabs:: + + .. tab:: Avoid + + .. code-block:: python + + if not x is None: + return x + + .. tab:: Use + + .. code-block:: python + + if x is not None: + return 'x exists!' + + +Also, avoid ``if x:`` when you mean ``if x is not None:``. This is +especially important when parsing arguments. + + +Handling Strings +~~~~~~~~~~~~~~~~ +Use ``.startswith()`` and ``.endswith()`` instead of slicing. + + +.. tabs:: + + .. tab:: Avoid + + .. code-block:: python + + if word[:3] == "cat": + print("The word starts with 'cat'.") + + if file_name[-4:] == ".jpg": + print("The file is a JPEG.") + + .. tab:: Use + + .. code-block:: python + + if word.startswith("cat"): + print("The word starts with 'cat'.") + + if file_name.endswith(".jpg"): + print("The file is a JPEG.") + + +Reading the Windows Registry +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Never read the Windows registry or write to it because this is dangerous and +makes it difficult to deploy libraries on different environments or operating +systems. + +.. tabs:: + + .. tab:: Avoid + + .. code-block:: python + + self.sDesktopinstallDirectory = Registry.GetValue( + "HKEY_LOCAL_MACHINE\Software\Ansoft\ElectronicsDesktop\{}\Desktop".format( + self.sDesktopVersion + ), + "InstallationDirectory", + "", + ) + + +Duplicated Code +~~~~~~~~~~~~~~~ +Follow the DRY principle, which states that "Every piece of knowledge +must have a single, unambiguous, authoritative representation within a +system." Follow this principle unless it overly complicates +the code. For instance, the following example converts Fahrenheit to Kelvin +twice, which now requires the developer to maintain two separate lines +that do the same thing. + + +.. tabs:: + + .. tab:: Avoid + + .. code-block:: python + + temp = 55 + new_temp = ((temp - 32) * (5 / 9)) + 273.15 + + temp2 = 46 + new_temp_k = ((temp2 - 32) * (5 / 9)) + 273.15 + + + .. tab:: Use + + .. code-block:: python + + def fahr_to_kelvin(fahr) + """Convert temperature in Fahrenheit to Kelvin. + + Parameters + ---------- + fahr : int or float + Temperature in Fahrenheit. + + Returns + ------- + kelvin : float + Temperature in Kelvin. + + """ + return ((fahr - 32) * (5 / 9)) + 273.15 + + new_temp = fahr_to_kelvin(55) + new_temp_k = fahr_to_kelvin(46) + + +This is a trivial example, but the approach can be applied for a +variety of both simple and complex algorithms and workflows. Another +advantage of this approach is that you can implement unit testing +for this method. + +.. code:: python + + import numpy as np + + + def test_fahr_to_kelvin(): + np.testing.assert_allclose(12.7778, fahr_to_kelvin(55)) + +Now, you have only one line of code to verify and can also use +a testing framework such as ``pytest`` to test that the method is +correct. + + +Nested Blocks +~~~~~~~~~~~~~ +Avoid deeply nested block structures (such as conditional blocks and loops) +within one single code block. + +.. code:: python + + def validate_something(self, a, b, c): + if a > b: + if a * 2 > b: + if a * 3 < b: + raise ValueError + else: + for i in range(10): + c += self.validate_something_else(a, b, c) + if c > b: + raise ValueError + else: + d = self.foo(b, c) + # recursive + e = self.validate_something(a, b, d) + + +Aside from the lack of comments, this complex method +is difficult to debug and validate with unit testing. It would +be far better to implement more validation methods and join conditional +blocks. + +For a conditional block, the maximum depth recommended is four. If you +think you need more for the algorithm, create small functions that are +reusable and unit-testable. + + +Loops +~~~~~ +While there is nothing inherently wrong with nested loops, to avoid +certain pitfalls, steer clear of having loops with more than two levels. In +some cases, you can rely on coding mechanisms like list comprehensions +to circumvent nested loops. + +.. tabs:: + + .. tab:: Avoid + + .. code-block:: python + + squares = [] + for i in range(10): + squares.append(i * i) + + .. code-block:: pycon + + >>> print(f"{squares = }") + squares = [0, 1, 4, 9, 16, 25, 36, 49, 64, 81] + + + .. tab:: Use + + .. code-block:: python + + squares = [i * i for i in range(10)] + + + .. code-block:: pycon + + >>> print(f"{squares = }") + squares = [0, 1, 4, 9, 16, 25, 36, 49, 64, 81] + + +If the loop is too complicated for creating a list comprehension, +consider creating small functions and calling these instead. For +example to extract all consonants in a sentence: + +.. tabs:: + + .. tab:: Avoid + + .. code-block:: python + + sentence = 'This is a sample sentence.' + vowels = 'aeiou' + consonants = [] + for letter in sentence: + if letter.isalpha() and letter.lower() not in vowels: + consonants.append(letter) + + .. code-block:: pycon + + >>> print(f"{consonants = }") + consonants = ['T', 'h', 's', 's', 's', 'm', 'p', 'l', 's', 'n', 't', 'n', 'c'] + + + .. tab:: Use + + .. code-block:: python + + def is_consonant(letter): + """Return ``True`` when a letter is a consonant.""" + vowels = 'aeiou' + return letter.isalpha() and letter.lower() not in vowels + + .. code-block:: pycon + + >>> sentence = "This is a sample sentence." + >>> consonants = [letter for letter in sentence if is_consonant(letter)] + >>> print(f"{consonants = }") + consonants = ['T', 'h', 's', 's', 's', 'm', 'p', 'l', 's', 'n', 't', 'n', 'c'] + +The second approach is more readable and better documented. Additionally, +you could implement a unit test for ``is_consonant``. + + +Security Considerations +----------------------- + +Security, an ongoing process involving people and practices, ensures application confidentiality, integrity, and availability [#]_. +Any library should be secure and implement good practices that avoid or mitigate possible security risks. +This is especially relevant in libraries that request user input (such as web services). +Because security is a broad topic, we recommend you review this useful Python-specific resource: + +* `10 Unknown Security Pitfalls for Python `_ - By Dennis Brinkrolf - Sonar source blog + +.. [#] Wikipedia - `Software development security `_. diff --git a/doc/source/coding-style/required-standard.rst b/doc/source/coding-style/required-standard.rst new file mode 100644 index 00000000..97f35a80 --- /dev/null +++ b/doc/source/coding-style/required-standard.rst @@ -0,0 +1,123 @@ +Required Standards +================== + +This section collects the required standards for any ``PyAnsys`` project. The +individual configurations for the tools presented in :ref:`Code Style Tools` and +:ref:`Doc Style Tools` are combined together. + +The following lines should be included in :ref:`The \`\`pyproject.toml\`\` File` +to indicate the configuration of the different code and documentation style tools. + + +Required ``pyproject.toml`` Config +---------------------------------- + +.. code-block:: toml + + [tool.black] + line-length: "" + + [tool.isort] + profile = "black" + force_sort_within_sections = true + line_length = "" + default_section = "THIRDPARTY" + src_paths = ["doc", "src", "tests"] + + [tool.coverage.run] + source = ["ansys."] + + [tool.coverage.report] + show_missing = true + + [tool.pytest.ini_options] + addopts = "--doctest-modules" + + [tool.pydocstyle] + convention = "numpy" + + +Required ``.flake8`` Config +--------------------------- +The following ``.flake8`` file is also required: + +.. code-block:: toml + + [flake8] + max-line-length = 88 + extend-ignore = E203 + + +Required ``pre-commit`` Config +------------------------------ +You can take advantage of :ref:`Pre-Commit` by including a +``.pre-commit-config.yaml`` file like the following one in your project: + + +.. code-block:: yaml + + repos: + + - repo: https://github.com/psf/black + rev: X.Y.Z + hooks: + - id: black + + - repo: https://github.com/pycqa/isort + rev: X.Y.Z + hooks: + - id: isort + + - repo: https://github.com/PyCQA/flake8 + rev: X.Y.Z + hooks: + - id: flake8 + + - repo: https://github.com/codespell-project/codespell + rev: vX.Y.Z + hooks: + - id: codespell + + - repo: https://github.com/pycqa/pydocstyle + rev: X.Y.Z + hooks: + - id: pydocstyle + additional_dependencies: [toml] + exclude: "tests/" + + +GitHub CI/CD integration +------------------------ +Finally, you can take advantage of :ref:`Unit Testing on GitHub via CI/CD` and +create a ``style.yml`` workflow file in ``.github/workflows/``: + +.. code-block:: yaml + + name: Style + + on: + pull_request: + push: + tags: + - "*" + branches: + - main + + jobs: + style: + name: Code & Doc + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Setup Python + uses: actions/setup-python@v2 + with: + python-version: '3.10' + - name: Install requirements + run: | + python -m pip install -U pip pre-commit + + - name: Run pre-commit + run: | + pre-commit run --all-files --show-diff-on-failure diff --git a/doc/source/coding_style/beyond_pep8.rst b/doc/source/coding_style/beyond_pep8.rst deleted file mode 100644 index b334d0d9..00000000 --- a/doc/source/coding_style/beyond_pep8.rst +++ /dev/null @@ -1,306 +0,0 @@ -Beyond PEP8 -########### -This topic describes any delineations, clarifications, or additional procedures above and -beyond `PEP8 `__. - -For example, `Stack Overflow Answer `_ -outlines deprecation best practices and a `Deprecation library `_ -already exists. However, there is no official guidance regarding deprecating features -in an API within Python. This page seeks to clarify this issue and others. - -Aside from the following PyAnsys-specific directives, the best coding practice is to -follow established guidelines from `PEP8 `__. - - -Deprecation Best Practice -------------------------- -Whenever a method, class, or function is deprecated, you must provide -an old method that both calls the new method and raises a warning. Or, -if the method has been removed, you must raise an ``AttributeError``. - -In the docstring of the older method, provide a `Sphinx Deprecated Directive -`_ -and a link to the newer method. This way, users are notified that an API change -has occurred and are given an opportunity to change their code. Otherwise, -stability concerns can cause users to stop upgrading, or worse, stop using -Ansys APIs. For this reason, it's best to use a warning first and then use -an error after a minor release or two. - - -.. code:: python - - class FieldAnalysis2D(): - """Class docstring""" - - def assignmaterial(self, obj, mat): - """Assign a material to one or more objects. - - .. deprecated:: 0.4.0 - Use :func:`FieldAnalysis2D.assign_material` instead. - - """ - # one of the following: - - # raise a DeprecationWarning. User won't have to change anything. - warnings.warn('assignmaterial is deprecated. Use assign_material instead.', - DeprecationWarning) - self.assign_material(obj, mat) - - # or raise an AttributeError (could also make a custom DeprecationError) - raise AttributeError('assignmaterial is deprecated. Use assign_material instead.') - - def assign_material(self, obj, mat): - """Assign a material to one or more objects. - ... - - -If a method is removed entirely, there's no reason to provide a link -to the old method. If the method is part of a class, raise an -``AttributeError``. Otherwise, raise an ``Exception``. - -This example raises an ``Exception``: - -.. code:: python - - def hello_world(): - """Print ``"hello_world"`` - - .. deprecated:: 0.4.0 - This function has been deprecated. - - """ - raise Exception('`my_function` has been deprecated.') - -Because there is no built-in deprecation error within -Python, an alternative is to create a custom ``DeprecationError``. - -.. code:: python - - class DeprecationError(RuntimeError): - """Used for depreciated methods and functions.""" - - def __init__(self, message='This feature has been depreciated.'): - """Empty init.""" - RuntimeError.__init__(self, message) - -You can then use this custom ``DeprecationError`` in place of an ``Exception``. - -.. code:: python - - def hello_world(): - """Print ``"hello_world"`` - - .. deprecated:: 0.4.0 - This function has been deprecated. - - """ - raise DeprecationError('`my_function` has been deprecated') - - -Semantic Versioning and API Changes ------------------------------------ -According to `Semantic Versioning `_, you should -increment the MAJOR version when you make incompatible changes. -However, adding or eliminating methods should not be considered -incompatible changes to a code base but rather incremental changes -that are backwards-compatible (to a degree). Therefore, whenever a -method or feature is added, changed, or removed, the minor version -should be bumped. - -To avoid constantly bumping the minor version, one approach to -source-control branching is to create release branches where only -patch fixes are pushed and API changes occur between minor -releases. See `Trunk Based Development -`_. - -In summary, the mainline branch (commonly named ``main`` or ``master``) -must always be ready to release, and developers should create -release branches to maintain at least one prior minor version. - -The reason behind this is if a user wants to use API 0.4.0 instead of -0.5.0 due to some pressing deadline where they want to avoid a code -refactor, the maintainers of the API can back-port a bug-fix via ``git -cherry-pick ``. This gives users some time to update any -projects dependent on the API while still treating them as -"first-class" users. - -Note that due to the complexity of maintaining multiple "release branches" -in a repository, the number of active release branches should be between -one and three. - -Docstring Examples Best Practice --------------------------------- -Defining docstring examples for methods and classes is extremely -useful. The examples give users an easy place to start when trying -out the API, showing them exactly how to operate on a method or -class. By using ``doctest`` through ``pytest``, docstring examples can -also be used to perform regression testing to verify that the code is -executing as expected. - -This is an important feature of maintainable documentation as examples -must always match the API they are documenting. When using ``doctest`` -through ``pytest``, any changes within the API without corresponding -changes in the documentation will trigger doctest failures. - -Setting Up ``doctest`` -~~~~~~~~~~~~~~~~~~~~~~ -First, install ``pytest``. - -.. code:: - - pip install pytest - -Now, run ``doctest`` on any Python file. - -.. code:: - - pytest --doctest-modules file.py - -``doctest`` searches for examples in the docstrings and executes them -to verify that they function as written. - -Using ``pytest`` Fixtures -~~~~~~~~~~~~~~~~~~~~~~~~~ -To define a setup sequence before the ``doctest`` run or before a given -module is tested, you use ``pytest`` fixtures. Because fixtures allow -docstring examples to access shared objects, there is no need to repeat -the setup in each example. - -``pytest`` fixtures can be defined in a ``conftest.py`` file next to the source -code. The following example shows a fixture that is run automatically for -each ``doctest`` session. - -.. code:: python - - import pytest - - from pyaedt import Desktop - - - @pytest.fixture(autouse=True, scope="session") - def start_aedt(): - desktop = Desktop("2021.1", NG=True) - desktop.disable_autosave() - - # Wait to run doctest on docstrings - yield desktop - desktop.force_close_desktop() - -Fixtures can also be defined in a separate Python file from -``conftest.py``. This may help keep the fixtures more organized. Fixtures -from other files need to be imported in the main ``conftest.py`` file. - -This example shows how to import fixtures defined in an -``icepak_fixtures.py`` file under the ``doctest_fixtures`` folder. - -.. code:: python - - import pytest - - from pyaedt import Desktop - from pyaedt.doctest_fixtures import * - - # Import fixtures from other files - pytest_plugins = [ - "pyaedt.doctest_fixtures.icepak_fixtures", - ] - - - @pytest.fixture(autouse=True, scope="session") - def start_aedt(): - desktop = Desktop("2021.1", NG=True) - desktop.disable_autosave() - - # Wait to run doctest on docstrings - yield desktop - desktop.force_close_desktop() - -The ``doctest_namespace`` fixture built into ``doctest`` allows injecting -items from a fixture into the context of the ``doctest`` run. To use this -feature, the fixture needs to accept the ``doctest_namespace`` dictionary -as an argument. Then, objects can be added to the ``doctest_namespace`` -dictionary and used directly in a docstring example. - -This examples shows how the ``Icepak`` object can be stored in the -``doctest_namespace`` dictionary by adding the key ``icepak`` with the -``Icepak`` object as the value. - -.. code:: python - - import pytest - from pyaedt import Icepak - - - @pytest.fixture(autouse=True, scope="module") - def create_icepak(doctest_namespace): - doctest_namespace["icepak"] = Icepak(projectname="Project1", designname="IcepakDesign1") - -The ``Icepak`` object can then be used directly inside a docstring -example by referencing the key ``icepak``. - -.. code:: python - - def assign_openings(self, air_faces): - """Assign openings to a list of faces. - - Parameters - ---------- - air_faces : list - List of face names. - - Returns - ------- - :class:`pyaedt.modules.Boundary.BoundaryObject` - Boundary object when successful or ``None`` when failed. - - Examples - -------- - - Create an opening boundary for the faces of the "USB_GND" object. - - >>> faces = icepak.modeler.primitives["USB_GND"].faces - >>> face_names = [face.id for face in faces] - >>> boundary = icepak.assign_openings(face_names) - pyaedt Info: Face List boundary_faces created - pyaedt Info: Opening Assigned - - """ - -Useful ``doctest`` Features -~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Ellipses for Random Output -************************** -If the output of some operation in an example cannot be verified exactly, -an ellipsis (``...``) can be used in the expected output. This allows it -to match any substring in the actual output. - -.. code :: - - Examples - -------- - - >>> desktop = Desktop("2021.1") - pyaedt Info: pyaedt v... - pyaedt Info: Python version ... - -To support this, ``doctest`` must be run with the option set to allow ellipses. - -.. code :: - - pytest --doctest-modules -o ELLIPSIS file.py - -``doctest`` Skip -**************** -The directive ``# doctest: +SKIP`` can be added to any line of a -docstring example so that it is not executed in ``doctest-modules``. -This is useful for examples that cannot run within ``pytest`` or have -side effects that will affect the other tests if they are run during -the ``doctest`` session. - -.. code :: python - - Examples - -------- - - >>> desktop = Desktop("2021.1") # doctest: +SKIP diff --git a/doc/source/coding_style/flake8.rst b/doc/source/coding_style/flake8.rst deleted file mode 100644 index 1780f642..00000000 --- a/doc/source/coding_style/flake8.rst +++ /dev/null @@ -1,252 +0,0 @@ -.. _style-guide-enforcement: - -Style Guide Enforcement -======================= -This topic describes the use of `Flake8`_ for `PEP8`_ style -enforcement and the minimum standards expected. PyAnsys libraries -are expected to be consistent with these guidelines. - -.. _PEP8: https://www.python.org/dev/peps/pep-0008/ - -Flake8 -~~~~~~ -`Flake8`_ is a Python tool for enforcing code styling. It is a wrapper -around the following three tools: `PyFlakes`_, `pycodestyle`_, and -`Ned Batchelder's McCabe script for complexity`_. Flake8 runs all three tools at once, -checking the code against a variety of style rules, such as line length, -code complexity, and whitespace. - -.. _Flake8: https://flake8.pycqa.org/en/latest/index.html -.. _PyFlakes: https://pypi.org/project/pyflakes/ -.. _pycodestyle: https://pypi.org/project/pycodestyle/ -.. _`Ned Batchelder's McCabe script for complexity`: https://github.com/PyCQA/mccabe -.. _configuring-flake8: - -Configuring Flake8 ------------------- -Flake8 supports configuring a specific set of style rules to -enforce. This configuration can be stored in your library in a -``setup.cfg``, ``tox.ini``, or ``.flake8`` file. PyAnsys libraries -store the Flake8 configuration in a ``.flake8`` file at the root of the -repository. - -Here is an example of a ``.flake8`` configuration file from a PyAnsys -library: - -.. code:: - - [flake8] - exclude = venv, __init__.py, doc/_build - select = W191, W291, W293, W391, E115, E117, E122, E124, E125, E225, E231, E301, E303, E501, F401, F403 - count = True - max-complexity = 10 - max-line-length = 100 - statistics = True - -Flake8 has many options that can be set within the configuration file. -For a list and descriptions, see this `Flake8 documentation topic -`__. - -The example configuration defines the following options: - -- ``exclude`` - Denotes subdirectories and ``doc/_build``, along with all - ``__init__.py`` files to be excluded from the check. - -- ``select`` - Sequence of error codes that Flake8 will report errors - for. The set in the above configuration is a basic set of errors to - check for and is not an exhaustive list. - - For a full list of error codes and their descriptions, see this `Flake8 - documentation topic `__. - -- ``count`` - Total number of errors to print at the end of the check. - -- ``max-complexity`` - Sets the maximum allowed McCabe complexity value for a block of code. - The value of 10 was chosen because it is a common default. - -- ``max-line-length`` - Denotes the maximum line length for any one line of code. - The `PEP8`_ standard advises a maximum line length of 79. Because - this is a bit limiting in some cases, the maximum line length - recommended for a PyAnsys library is 100. - -- ``statistics`` - Number of occurrences of each error or warning code - to be printed as a report at the end of the check. - - -Running Flake8 --------------- -First, to install Flake8, run: - -.. code:: - - python -m pip install flake8 - -Then, you can run Flake8 from inside your project directory by executing: - -.. code:: - - flake8 . - -This uses the configuration defined in the ``.flake8`` file to -run the style checks on the appropriate files within the project and -report any errors. - -In PyAnsys libraries, Flake8 is run as part of the CI/CD for code style. -This action is run as a required check on pull requests, preventing -code in violation of style rules from being merged into the code -base. - - -Utilizing Black -~~~~~~~~~~~~~~~ -Manually checking for code styling can be a tedious task. Luckily, -several Python tools for auto-formatting code to meet PEP8 standards -are available to help with this. The PyAnsys project suggests the use of the -the formatting tool `black`_. - -On completing a code change, and before committing, `black`_ can be -run to reformat the code, following the PEP8 guidelines enforced through -Flake8. This will limit any manual code changes needed to address style -rules. - -.. _black: https://black.readthedocs.io/en/stable/ - -Optionally, it is possible to automate the use of `black`_. This can be -done with the tool `pre-commit`_. Setting up a `pre-commit hook -to run black `_ -will automatically format the code before committing. This simple way of -incorporating code style checks into the development workflow to maintain -PEP8 guidelines requires minimal manual effort. - -.. _pre-commit: https://pre-commit.com/ - - -Minimum Standards -~~~~~~~~~~~~~~~~~ -The following section describes the minimum set of code style standards -expected in a PyAnsys library. - -* `W191`_ - **Indentation contains tabs.** - - Indentations should be composed of four spaces, not tabs. - -* `W291`_ - **Trailing whitespace.** - - There should be no trailing whitespace after the final character - on a line. - -* `W293`_ - **Blank line contains whitespace.** - - Blank lines should not have any tabs or spaces. - -* `W391`_ - **Blank line at the end of every file.** - - There should be only one blank line at the end of each file. This - warning will occur when there are zero, two, or more than two blank - lines. - -* `E115`_ - **Comment block expected an indent.** - - An indented block comment was expected but a non-indented block - comment was found instead. - -* `E117`_ - **Line over-indented.** - - Lines should be consistently indented in increments of two or four. - -* `E122`_ - **Continuation line missing indentation or outdented.** - - Continuation line is not indented as far as it should be or is - indented too far. - -* `E124`_ - **Closing bracket does not match indentation.** - - Closing bracket does not match the indentation of the opening bracket. - -* `E125`_ - **Continuation line with same indent as next logical line.** - - Continuation line is indented at the same level as the next logical - line. It should be indented to one more level to distinguish it from - the next line. - -* `E225`_ - **Missing whitespace around operator.** - - There should be one space before and after all operators. - -* `E231`_ - **Missing whitespace after certain special characters.** - - There should be one space after the characters ``,``, ``;``, and ``:``. - -* `E301`_ - **Expected a blank line, found none.** - - All methods of a class should have a single line between them. - -* `E303`_ - **Too many blank lines.** - - There should be one line between methods and two lines between - methods and classes. - -* `E501`_ - **Line too long.** - - All code lines should not exceed 100 characters. The - `PEP8 line length guideline `_ - suggests a maximum line length of 79. Following this limit - is not as necessary today due to modern screen sizes. The suggested maximum - length of 100 can be easier to accommodate and can still support - viewing files side by side in code editors. - -* `F401`_ - **Module imported but unused.** - - Modules should only be imported if they are actually used. - -* `F403`_ - **'from module import *' used.** - - Importing using wildcards (``*``) should never be done. Importing - modules this way leads to uncertainty and pollutes the code. You - cannot know exactly what is being imported and name clashes are common. - Import only the modules to be used. - -* **Limit complexity of code to 10.** - - This is enforced by the ``max-complexity`` option described in - :ref:`configuring-flake8`. Limiting code complexity leads to code that - is easier to understand and less risky to modify. Write low- - complexity code when possible. - - -Your ``.flake8`` file should be: - -.. code:: - - [flake8] - exclude = venv, __init__.py, doc/_build - select = W191, W291, W293, W391, E115, E117, E122, E124, E125, E225, E231, E301, E303, E501, F401, F403 - count = True - max-complexity = 10 - max-line-length = 100 - statistics = True - - -.. _W191: https://www.flake8rules.com/rules/W191.html -.. _W291: https://www.flake8rules.com/rules/W291.html -.. _W293: https://www.flake8rules.com/rules/W293.html -.. _W391: https://www.flake8rules.com/rules/W391.html -.. _E115: https://www.flake8rules.com/rules/E115.html -.. _E117: https://www.flake8rules.com/rules/E117.html -.. _E122: https://www.flake8rules.com/rules/E122.html -.. _E124: https://www.flake8rules.com/rules/E124.html -.. _E125: https://www.flake8rules.com/rules/E125.html -.. _E225: https://www.flake8rules.com/rules/E225.html -.. _E231: https://www.flake8rules.com/rules/E231.html -.. _E301: https://www.flake8rules.com/rules/E301.html -.. _E303: https://www.flake8rules.com/rules/E303.html -.. _E501: https://www.flake8rules.com/rules/E501.html -.. _F401: https://www.flake8rules.com/rules/F401.html -.. _F403: https://www.flake8rules.com/rules/F403.html - diff --git a/doc/source/coding_style/index.rst b/doc/source/coding_style/index.rst deleted file mode 100644 index 5a59528f..00000000 --- a/doc/source/coding_style/index.rst +++ /dev/null @@ -1,29 +0,0 @@ -.. _coding_style: - -Coding Style -************ - -PyAnsys libraries are expected to follow `PEP8`_ and -be consistent in style and formatting with the 'big three' -data science libraries: `NumPy`_, `SciPy`_, and `pandas`_. - -.. _NumPy: https://numpy.org/ -.. _SciPy: https://www.scipy.org/ -.. _pandas: https://pandas.pydata.org/ -.. _PEP8: https://www.python.org/dev/peps/pep-0008/ - - -.. todo:: - - * Describe flake8 standards (in subpage), include example ``.flake8`` - with minimum standards. - * Include anything we've written from other documentation either in - this page or other pages. - -.. toctree:: - :hidden: - :maxdepth: 3 - - pep8_best_practices - beyond_pep8 - flake8 diff --git a/doc/source/coding_style/pep8_best_practices.rst b/doc/source/coding_style/pep8_best_practices.rst deleted file mode 100644 index 2d8b2524..00000000 --- a/doc/source/coding_style/pep8_best_practices.rst +++ /dev/null @@ -1,724 +0,0 @@ -.. _best_practices: - -PEP 8 Best Practices -==================== -This topic summarizes key points from `PEP8`_ and how -they apply to PyAnsys libraries. The goal is for PyAnsys libraries to -be consistent in style and formatting with the `big three` -data science libraries: `NumPy`_, `SciPy`_, and `pandas`_. - -.. _NumPy: https://numpy.org/ -.. _SciPy: https://www.scipy.org/ -.. _pandas: https://pandas.pydata.org/ -.. _PEP8: https://www.python.org/dev/peps/pep-0008/ - - -Imports -~~~~~~~ -Imports should always be placed at the top of the file, just after any -module comments and docstrings and before module globals and -constants. This reduces the likelihood of an `ImportError`_ that -might only be discovered during runtime. - -.. _ImportError: https://docs.python.org/3/library/exceptions.html#ImportError - -Instead of: - -.. code:: python - - def compute_logbase8(x): - import math - return math.log(8, x) - -Use: - -.. code:: python - - import math - - def compute_logbase8(x): - return math.log(8, x) - - -For better readability, group imports in this order: - -#. Standard library imports -#. Related third-party imports -#. Local application- or library-specific imports - -Instead of: - -.. code:: python - - import sys - import subprocess - from mypackage import mymodule - import math - - def compute_logbase8(x): - return math.log(8, x) - - -Use: - -.. code:: python - - import sys - import subprocess - import math - from mypackage import mymodule - - def compute_logbase8(x): - return math.log(8, x) - - -You should place imports in separate lines unless they are -modules from the same package. - -Instead of: - -.. code:: python - - import sys, math - from my_package import my_module - from my_package import my_other_module - - def compute_logbase8(x): - return math.log(8, x) - -Use: - -.. code:: python - - import sys - import math - from my_package import my_module, my_other_module - - def compute_logbase8(x): - return math.log(8, x) - - -You should avoid using wild cards in imports because doing so -can make it difficult to detect undefined names. For more information, -see `Python Anti-Patterns: using wildcard imports <(https://docs.quantifiedcode.com/python-anti-patterns/maintainability/from_module_import_all_used.html>`_. - -Instead of: - -.. code:: python - - from my_package.mymodule import * - -Use: - -.. code:: python - - from my_package.my_module import myclass - - -Indentation and Line Breaks ---------------------------- -Proper and consistent indentation is important to producing -easy-to-read and maintainable code. In Python, use four spaces per -indentation level and avoid tabs. - -Indentation should be used to emphasize: - - - Body of a control statement, such as a loop or a select statement - - Body of a conditional statement - - New scope block - -.. code:: python - - class MyFirstClass: - """MyFirstClass docstring""" - - class MySecondClass: - """MySecondClass docstring""" - - def top_level_function(): - """Top level function docstring""" - return - -For improved readability, add blank lines or wrapping lines. Two -blank lines should be added before and after all function and class -definitions. - -Inside a class, use a single line before any method definition. - -.. code:: python - - class MyClass: - """MyClass docstring""" - - def first_method(self): - """First method docstring""" - return - - def second_method(self): - """Second method docstring""" - return - -To make it clear when a 'paragraph' of code is complete and a new section -is starting, use a blank line to separate logical sections. - -Instead of: - -.. code:: - - if x < y: - - STATEMENTS_A - - else: - - if x > y: - - STATEMENTS_B - - else: - - STATEMENTS_C - - if x > 0 and x < 10: - - print("x is a positive single digit.") - -Use: - -.. code:: - - if x < y: - STATEMENTS_A - else: - if x > y: - STATEMENTS_B - else: - STATEMENTS_C - - if x > 0 and x < 10: - print("x is a positive single digit.") - elif x < 0: - print("x is less than zero.") - - -Maximum Line Length -------------------- -For source code lines, best practice is to keep the length at or below -100 characters. For docstrings and comments, best practice is to keep -the length at or below 72 characters. - -Lines longer than these recommended limits might not display properly -on some terminals and tools or might be difficult to follow. For example, -this line is difficult to follow: - -.. code:: python - - employee_hours = [schedule.earliest_hour for employee in self.public_employees for schedule in employee.schedules] - -The line can be rewritten as: - -.. code:: python - - employee_hours = [schedule.earliest_hour for employee in - self.public_employees for schedule in employee.schedules] - -Alternatively, instead of writing a list comprehension, you can use a -classic loop. - - -Naming Conventions ------------------- -To achieve readable and maintainable code, use concise and descriptive names for classes, -methods, functions, and constants. Regardless of the programming language, you must follow these -global rules to determine the correct names: - -#. Choose descriptive and unambiguous names. -#. Make meaningful distinctions. -#. Use pronounceable names. -#. Use searchable names. -#. Replace magic numbers with named constants. -#. Avoid encodings. Do not append prefixes or type information. - - -Names to Avoid -~~~~~~~~~~~~~~ -Do not use the characters ``'l'``, ``'O'`` , or ``'I'`` as -single-character variable names. In some fonts, these characters are -indistinguishable from the numerals one and zero. - - -Package and Module Naming Conventions -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Use a short, lowercase word or words for module names. Separate words -with underscores to improve readability. For example, use ``module.py`` -or ``my_module.py``. - -For a package name, use a short, lowercase word or words. Avoid -underscores as these must be represented as dashes when installing -from PyPi. - -.. code:: - - pip install package - - -Class Naming Conventions -~~~~~~~~~~~~~~~~~~~~~~~~ -Use `camel case `_ when naming classes. Do not separate words -with underscores. - -.. code:: python - - class MyClass(): - """Docstring for MyClass""" - pass - - -Function and Method Naming Conventions -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Use a lowercase word or words for Python functions or methods. Separate -words with underscores to improve readability. - -.. code:: python - - class MyClass(): - """Docstring for MyClass""" - - def __init__(self, value): - """Constructor. - - Methods with double underscores on either side are called - "dunder" methods and are special Python methods. - - """ - self._value = value - - def __private_method(self): - """This method can only be called from ``MyClass``.""" - self._value = 0 - - def _protected_method(self): - """This method should only be called from ``MyClass``. - - Protected methods can be called from inherited classes, - unlike private methods, which names are 'mangled' to avoid - these methods from being called from inherited classes. - - """ - # note how we can call private methods here - self.__private_method() - - def public_method(self): - """This method can be called external to this class.""" - self._value += 2 - - -Variable Naming Conventions -~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Use a lowercase single letter, word, or words when naming -variables. Separate words with underscores to improve readability. - -.. code:: python - - my_variable = 5 - - -Constants are variables that are set at the module level and are used -by one or more methods within that module. Use an uppercase word or -words for constants. Separate words with underscores to improve -readability. - -.. code:: python - - PI = 3.141592653589793 - CONSTANT = 4 - MY_CONSTANT = 8 - MY_OTHER_CONSTANT = 1000 - - -Comments -~~~~~~~~ -Because a PyAnsys library generally involves multiple physics domains, -users reading its source code do not have the same background as -the developers who wrote it. This is why it is important for a library -to have well commented and documented source code. Comments that -contradict the code are worse than no comments. Always make a priority -of keeping comments up to date with the code. - -Comments should be complete sentences. The first word should be -capitalized, unless it is an identifier that begins with a lowercase -letter. - -Here are general guidelines for writing comments: - -#. Always try to explain yourself in code by making it - self-documenting with clear variable names. -#. Don't be redundant. -#. Don't add obvious noise. -#. Don't use closing brace comments. -#. Don't comment out code that is unused. Remove it. -#. Use explanations of intent. -#. Clarify the code. -#. Warn of consequences. - -Obvious portions of the source code should not be commented. -For example, the following comment is not needed: - -.. code:: python - - # increment the counter - i += 1 - -However, an important portion of the behavior that is not self-apparent -should include a note from the developer writing it. Otherwise, -future developers may remove what they see as unnecessary. - -.. code:: python - - # Be sure to reset the object's cache prior to exporting. Otherwise, - # some portions of the database in memory will not be written. - obj.update_cache() - obj.write(filename) - - -Inline Comments -~~~~~~~~~~~~~~~ -Use inline comments sparingly. An inline comment is a comment on the -same line as a statement. - -Inline comments should be separated by two spaces from the statement. - -.. code:: python - - x = 5 # This is an inline comment - -Inline comments that state the obvious are distracting and should be -avoided: - -.. code:: python - - x = x + 1 # Increment x - - -Focus on writing self-documenting code and using short but -descriptive variable names. - -Instead of: - -.. code:: python - - x = 'John Smith' # Student Name - -Use: - -.. code:: python - - user_name = 'John Smith' - - -Docstring Conventions -~~~~~~~~~~~~~~~~~~~~~ -A docstring is a string literal that occurs as the first statement in -a module, function, class, or method definition. A docstring becomes -the doc special attribute of the object. - -Write docstrings for all public modules, functions, classes, and -methods. Docstrings are not necessary for private methods, but such -methods should have comments that describe what they do. - -To create a docstring, surround the comments with three double quotes -on either side. - -For a one-line docstring, keep both the starting and ending ``"""`` on the -same line: - -.. code:: python - - """This is a docstring.""". - -For a multi-line docstring, put the ending ``"""`` on a line by itself. - -PyAEDT follows the `numpydoc -`_ -docstring style, which is used by `numpy `_, -`scipy `_, `pandas -`_, and a variety of other Python open -source projects. For more information on docstrings for PyAnsys -libraries, see :ref:`Documentation Style`. - - -Programming Recommendations -~~~~~~~~~~~~~~~~~~~~~~~~~~~ -The following sections provide some `PEP8 -`_ suggestions for removing -ambiguity and preserving consistency. They address some common pitfalls -when writing Python code. - - -Booleans and Comparisons ------------------------- -Don't compare Boolean values to ``True`` or ``False`` using the -equivalence operator. - -Instead of: - -.. code:: python - - if my_bool == True: - return result - -Use: - -.. code:: python - - if my_bool: - return result - -Knowing that empty sequences are evaluated to ``False``, don't compare the -length of these objects but rather consider how they would evaluate -by using ``bool()``. - -Instead of: - -.. code:: python - - my_list = [] - if not len(my_list): - raise ValueError('List is empty') - -Use: - -.. code:: python - - my_list = [] - if not my_list: - raise ValueError('List is empty') - -In ``if`` statements, use ``is not`` rather than ``not ...``. - -Instead of: - -.. code:: python - - if not x is None: - return x - -Use: - -.. code:: python - - if x is not None: - return 'x exists!' - -Also, avoid ``if x:`` when you mean ``if x is not None:``. This is -especially important when parsing arguments. - - -Handling Strings ----------------- -Use ``.startswith()`` and ``.endswith()`` instead of slicing. - -Instead of: - -.. code:: python - - if word[:3] == 'cat': - print('The word starts with "cat"') - - if file_name[-3:] == 'jpg': - print('The file is a JPEG') - -Use: - -.. code:: python - - if word.startswith('cat'): - print('The word starts with "cat"') - - if file_name.endswith('jpg'): - print('The file is a JPEG') - - -Reading the Windows Registry ----------------------------- -Never read the Windows registry or write to it because this is dangerous and -makes it difficult to deploy libraries on different environments or operating -systems. - -Bad practice - Example 1 - -.. code:: python - - self.sDesktopinstallDirectory = Registry.GetValue("HKEY_LOCAL_MACHINE\Software\Ansoft\ElectronicsDesktop\{}\Desktop".format(self.sDesktopVersion), "InstallationDirectory", '') - -Bad practice - Example 2 - -.. code:: python - - EMInstall = (string)Registry.GetValue(string.Format(@"HKEY_LOCAL_MACHINE\SOFTWARE\Ansoft\ElectronicsDesktop{0}\Desktop", AnsysEmInstall.DesktopVersion), "InstallationDirectory", null); - - -Duplicated Code ---------------- -Follow the DRY principle, which states that "Every piece of knowledge -must have a single, unambiguous, authoritative representation within a -system." Attempt to follow this principle unless it overly complicates -the code. For instance, the following example converts Fahrenheit to kelvin -twice, which now requires the developer to maintain two separate lines -that do the same thing. - -.. code:: python - - temp = 55 - new_temp = ((temp - 32) * (5 / 9)) + 273.15 - - temp2 = 46 - new_temp_k = ((temp2 - 32) * (5 / 9)) + 273.15 - -Instead, write a simple method that converts Fahrenheit to kelvin: - -.. code:: python - - def fahr_to_kelvin(fahr) - """Convert temperature in Fahrenheit to kelvin. - - Parameters: - ----------- - fahr: int or float - Temperature in Fahrenheit. - - Returns: - ----------- - kelvin : float - Temperature in kelvin. - """ - return ((fahr - 32) * (5 / 9)) + 273.15 - -Now, you can execute and get the same output with: - -.. code:: python - - new_temp = fahr_to_kelvin(55) - new_temp_k = fahr_to_kelvin(46) - -This is a trivial example, but the approach can be applied for a -variety of both simple and complex algorithms and workflows. Another -advantage of this approach is that you can implement unit testing -for this method. - -.. code:: python - - import numpy as np - - def test_fahr_to_kelvin(): - assert np.isclose(12.7778, fahr_to_kelvin(55)) - -Now, not only do you have one line of code to verify, but you can also -use a testing framework such as ``pytest`` to test that the method is -correct. - - -Nested Blocks -------------- -Avoid deeply nested block structures (such as conditional blocks and loops) -within one single code block. - -.. code:: python - - def validate_something(self, a, b, c): - if a > b: - if a*2 > b: - if a*3 < b: - raise ValueError - else: - for i in range(10): - c += self.validate_something_else(a, b, c) - if c > b: - raise ValueError - else: - d = self.foo(b, c) - # recursive - e = self.validate_something(a, b, d) - - -Aside from the lack of comments, this complex method -is difficult to debug and validate with unit testing. It would -be far better to implement more validation methods and join conditional -blocks. - -For a conditional block, the maximum depth recommended is four. If you -think you need more for the algorithm, create small functions that are -reusable and unit-testable. - - -Loops ------ -While there is nothing inherently wrong with nested loops, to avoid -certain pitfalls, steer clear of having loops with more than two levels. In -some cases, you can rely on coding mechanisms like list comprehensions -to circumvent nested loops. - -Instead of: - -.. code:: - - >>> squares = [] - >>> for i in range(10): - ... squares.append(i * i) - >>> squares - [0, 1, 4, 9, 16, 25, 36, 49, 64, 81] - - -Implement a list comprehension with: - -.. code:: - - >>> squares = [i*i for i in range(10)] - >>> squares - [0, 1, 4, 9, 16, 25, 36, 49, 64, 81] - - -If the loop is too complicated for creating a list comprehension, -consider creating small functions and calling these instead. For -example, extract all consonants in a sentence: - -.. code:: python - - >>> sentence = 'This is a sample sentence.' - >>> vowels = 'aeiou' - >>> consonants = [] - >>> for letter in sentence: - ... if letter.isalpha() and letter.lower() not in vowels: - ... consonants.append(letter) - >>> consonants - ['T', 'h', 's', 's', 's', 'm', 'p', 'l', 's', 'n', 't', 'n', 'c'] - - -This is better implemented by creating a simple method to return if a -letter is a consonant: - - >>> def is_consonant(letter): - ... """Return ``True`` when a letter is a consonant.""" - ... vowels = 'aeiou' - ... return letter.isalpha() and letter.lower() not in vowels - ... - >>> sentence = 'This is a sample sentence.' - >>> consonants = [letter for letter in sentence if is_consonant(letter)] - >>> consonants - ['T', 'h', 's', 's', 's', 'm', 'p', 'l', 's', 'n', 't', 'n', 'c'] - -The second approach is more readable and better documented. Additionally, -you could implement a unit test for ``is_consonant``. - - -Security Considerations -~~~~~~~~~~~~~~~~~~~~~~~ - -Security, an ongoing process involving people and practices, ensures application confidentiality, integrity, and availability [#]_. -Any library should be secure and implement good practices that avoid or mitigate possible security risks. -This is especially relevant in libraries that request user input (such as web services). -Because security is a broad topic, we recommend you review this useful Python-specific resource: - -* `10 Unknown Security Pitfalls for Python `_ - By Dennis Brinkrolf - Sonar source blog - -.. [#] Wikipedia - `Software development security `_. diff --git a/doc/source/doc-style/formatting-tools.rst b/doc/source/doc-style/formatting-tools.rst index 87f29edb..626ecb6b 100644 --- a/doc/source/doc-style/formatting-tools.rst +++ b/doc/source/doc-style/formatting-tools.rst @@ -97,10 +97,7 @@ list of extensions: .. code-block:: python - extensions = [ - 'numpydoc', - ... - ] + extensions = ["numpydoc", ...] Once the `numpydoc`_ extension is added, you can select which `validation checks `_ diff --git a/doc/source/guidelines/dev_practices.rst b/doc/source/guidelines/dev_practices.rst index b39d22ac..77d74e79 100644 --- a/doc/source/guidelines/dev_practices.rst +++ b/doc/source/guidelines/dev_practices.rst @@ -11,7 +11,7 @@ coding paradigms: gained by following the basic guidelines listed in PEP 20. As suggested in these guidelines, focus on making your additions intuitive, novel, and helpful for PyAnsys users. When in doubt, use ``import this``. - For Ansys code quality standards, see :ref:`coding_style`. + For Ansys code quality standards, see :ref:`Coding Style`. #. Document your contributions. Include a docstring for any added function, method, or class, following `numpydocs docstring `_ diff --git a/doc/source/guidelines/version_support.rst b/doc/source/guidelines/version_support.rst index b9a990f2..bf7c9195 100644 --- a/doc/source/guidelines/version_support.rst +++ b/doc/source/guidelines/version_support.rst @@ -34,10 +34,7 @@ required Python version within ``setup.py`` with: [...] - setup(name="my_package_name", - python_requires='>3.6', - [...] - ) + setup(name="my_package_name", python_requires=">3.6", [...]) This helps ``pip`` to know which versions of your library support which versions of Python. You can also impose an upper limit if you're diff --git a/doc/source/index.rst b/doc/source/index.rst index ab347b9f..6fb50cdc 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -10,6 +10,6 @@ overview/index guidelines/index packaging/index - coding_style/index + coding-style/index doc-style/index abstractions/index diff --git a/doc/source/overview/administration.rst b/doc/source/overview/administration.rst index feb7fae8..f1a7b110 100644 --- a/doc/source/overview/administration.rst +++ b/doc/source/overview/administration.rst @@ -43,7 +43,7 @@ description, or branch protection management. Each repository is expected to follow this minimum set of standards: -- PEP8 code standards. See :ref:`best_practices`. +- PEP8 code standards. See :ref:`PEP 8`. - CI/CD using GitHub actions or Azure DevOps to enforce coding standards. - Publicly hosted documentation describing the API and providing examples. See :ref:`Documentation Style`. diff --git a/doc/source/overview/contributing.rst b/doc/source/overview/contributing.rst index fa630f21..e1c5093a 100644 --- a/doc/source/overview/contributing.rst +++ b/doc/source/overview/contributing.rst @@ -82,7 +82,7 @@ When you are ready to start contributing code, see: - :ref:`development_practices` for information on how PyAnsys development is conducted -- :ref:`best_practices` for information on how to style and format your +- :ref:`PEP 8` for information on how to style and format your code to adhere to PyAnsys standards diff --git a/doc/source/packaging/code/setup_file_code.rst b/doc/source/packaging/code/setup_file_code.rst index 3fce14eb..39759c80 100644 --- a/doc/source/packaging/code/setup_file_code.rst +++ b/doc/source/packaging/code/setup_file_code.rst @@ -1,9 +1,9 @@ .. code:: python """Installation file for ansys-mapdl-core.""" - + from setuptools import find_namespace_packages, setup - + setup( name="ansys--", packages=find_namespace_packages(where="src", include="ansys*"),