Skip to content

Latest commit

 

History

History

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 
 
 
 
 
 
 
 
 

README.md

Python_Toolkit

The code contained here is intended to be used in conjunction with the Python_Toolkit, as part of a wider BHoM installation. Its main purpose is to augment capabilities of the BHoM.

BHoM Python environments

Where guidelines are not explicitly given, code style standards and procedures should refer to the wider BHoM code compliance guidance.

Creation of a Python environment

A BHoM toolkit can be associated with a Python environment. The toolkit needs to have a location where Python code associated with that toolkit exists - so the first step is in creating that location and structure.

In the example below we'll go through creating a Python environment for the Example_Toolkit.

Structure

All Python code within BHoM should be placed within the hosting toolkit, inside its *_Engine folder. Within this folder, the following directory structure should be created (files have been included here within ./example_toolkit, though these are purely indicative and struictures can vary based on toolkit need):

.
├── ...
├── Example_Engine
│   └── Python
|       ├── README.md
|       ├── requirements.txt
|       ├── setup.py
|       ├── src
|       |   └── example_toolkit
|       |       ├── __init__.py
|       |       ├── module_a
|       |       |   ├── __init__.py
|       |       |   └── ...
|       |       └── ...
|       └── tests
|           ├── __init__.py
|           ├── test_module_a.py
|           └── ...
└── ...

setup.py

The setup.py file must be created alongside the code within a toolkit. This should contain teh following:

## setup.py ##
from pathlib import Path

import setuptools
from win32api import HIWORD, LOWORD, GetFileVersionInfo

TOOLKIT_NAME = "Example_Toolkit"

def _bhom_version() -> str:
    """Return the version of BHoM installed (using the BHoM.dll in the root BHoM directory."""
    info = GetFileVersionInfo("C:/ProgramData/BHoM/Assemblies/BHoM.dll", "\\")  # pylint: disable=[no-name-in-module]
    ms = info["FileVersionMS"]
    ls = info["FileVersionLS"]
    return f"{HIWORD(ms)}.{LOWORD(ms)}.{HIWORD(ls)}.{LOWORD(ls)}"  # pylint: disable=[no-name-in-module]

BHOM_VERSION = _bhom_version()

here = Path(__file__).parent.resolve()
long_description = (here / "README.md").read_text(encoding="utf-8")
requirements = [
    i.strip()
    for i in (here / "requirements.txt").read_text(encoding="utf-8-sig").splitlines()
]

setuptools.setup(
    name=TOOLKIT_NAME.lower(),
    author="BHoM",
    author_email="bhombot@burohappold.com",
    description=f"A Python library that contains code intended to be used by the {TOOLKIT_NAME} Python environment for BHoM workflows.",
    long_description=long_description,
    long_description_content_type="text/markdown",
    url=f"https://github.com/BHoM/{TOOLKIT_NAME}",
    package_dir={"": "src"},
    packages=setuptools.find_packages(where="src", exclude=['tests']),
    install_requires=requirements,
    version=BHOM_VERSION,
)

Enabling installation

Two methods are required in the toolkit to which Python code is being added. The first is a Query method for the name of that toolkit:

using BH.oM.Base.Attributes;

using System.ComponentModel;

namespace BH.Engine.Example
{
    public static partial class Query
    {
        [Description("Get the name of the current toolkit.")]
        [Output("name", "The name of the current toolkit.")]
        public static string ToolkitName()
        {
            return "Example_Toolkit";
        }
    }
}

And the second is a Compute method called ExampleToolkitPythonEnvironment which installs the toolkits associated Python environment. In the method below, Python v3.7.9 is being used for this toolkits Python environment:

using BH.oM.Base.Attributes;
using BH.oM.Python;

using System.ComponentModel;

namespace BH.Engine.Example
{
    public static partial class Compute
    {
        [Description("Install the Python virtualenv for the Example_Toolkit.")]
        [Input("run", "Run the installation process for this BHoM Python environment.")]
        [Output("env", "The Example_Toolkit Python environment, including any locally referenced BHoM code.")]
        public static PythonEnvironment ExampleToolkitPythonEnvironment(bool run = false)
        {
            if (run)
            {
                return BH.Engine.Python.Compute.InstallVirtualenv(
                    name: Query.ToolkitName(),
                    pythonVersion: BH.oM.Python.Enums.PythonVersion.v3_7_9,  // the version of Python to be used for this toolkits Python environment
                    localPackage: Path.Combine(Engine.Python.Query.CodeDirectory(), Query.ToolkitName()),
                    run: run
                );
            }
            return null;
        }
    }
}

Variations upon this can be made to reference external environments with known Python versions and installed packages also - with the LadybugTools_Toolkit showing how this is done.

Structure of Python code

Unless otherwise specified, code style must follow guidance from PEP8, unless otherwise stated in this document or in BHoM C# guidance (though some aspects of C# guidance are not applicable here due to differences between the two languages).

BHoM/Python specific style guidance

  • One method or class per file.
  • As much as possible, your code should be logically organised into a collection of directories and subdirectories.
  • Helper methods often don't fit into specific workflows and exist outside of packaged Python modules. These should be placed in a ./helpers directory in the root of the toolkits Python code.
  • Files named in snake_case. For example, a class called AThing, would be in a file called a_thing.py, and a method called do_stuff would be in a files called do_stuff.py.
  • Imports at the top of files must be absolute, to remove all ambiguity.
  • Type hints are required on all functions and classes (both inputs and outputs).
  • Docstrings must be added for all classes and methods. If the method is wrapped by a class, then only a description of that method is required as a minimum (unless the method is a dunder method).
  • Docstrings should be provided in Google format.
  • Default values should only be used where the defaults given are expected by a typical user of that code. If in doubt do not include a default.
  • Where defaults are given, these are best placed in the method/class instantiation rather than reassigning to that variable upon checking if variable "is None" later in the code.
  • Errors should be handled gracefully. When errors happen, the user should be told what went wrong and how to fix it. Broad Exceptions are allowed, although specific errors are preferable. Errors should also be captured at the lowest-possible level.

Testing

The following guidelines are given for implementation of Python code testing. It is anticipated that the majority of testing can be undertaken automatically without need for human interaction. This does increase the time required to develop code - but will result in far better code overall as a result. Everything that can be reasonably tested should be done so. Dispensation may be given from the CI/CD lead where appropriate following suitable justification made by the code developer.

The testing framework used in all BHoM Python code is pytest. In each BHoM toolkit, in the base of that toolkit is a ./.ci directory wherein tests should be placed. For example, in the Example_Toolkit, tests would be located in ./Example_Toolkit/.ci/unit_tests/python. Tests should be created by the person that writes the code that is being tested, and subject to review by those undertaking the PR review. A passing unit test is required for PR approval. The reviewer needs to ensure that the testing procedure is sufficiently comprehensive as part of their approval.

An example is given below for an appropriate candidate for testing:

## example_code.py ##
from typing import Union
def add(a: Union[int, float], b: Union[int, float]) -> float:
    """Add two numbers together.

    Args:
        a (Union[int, float]): The first number.
        b (Union[int, float]): The second number.

    Returns:
        Union[int, float]: The result of adding the inputs together.
    """
    if (not isinstance(a, (int, float))) and (not isinstance(b, (int, float)):
        raise TypeError(f"The combination of dtypes ({type(a)}, {type(b)}) passed are not valid for this method.")
    return a + b

... with testing procedures defined to check that this method works as expected ...

## test_example_code.py ##
import pytest
from example_code import add

def test_add():
    a = 1.5
    b = 2.5
    assert add(a, b) == 4.0

def test_add_fails():
    with pytest.raises(TypeError) as e_info:
        add("1", 1)

... and the command used to test this would be ...

python -m pytest ./test_example_code.py

Tests should be written so that:

  • Cases where a method runs correctly and returns a pre-defined result pass.
  • Cases where the method handles errors gracefully and raises an expected error pass.
  • In complex processes, tests may not be possible for all methods; however, it is expected that the lowest methods in that process are tested to reduce risk of error cascading through these processes.
  • In complex processes that rely on external programs, assertions should be made at the beginning of testing that those programs are available to the testing methods.
  • Where stochastic results are expected, use "fuzzy matching" to check for closeness of results (see pytest.approx)

Recommended development environment

Python in the context of BHoM is intended to be used where C# may not be as rapid or best placed to give results. As such, it can be considered more of a "scripting" language - with scope for formalisation into properly formatted and discretised code as part of BHoM toolkits. Ultimately it is up to the developer how to write their code, but the following setup is suggested for those just starting.

IDE

Use VSCode for Python code development. This editor provides extensibility via packages which enable the automation of a lot of repetitive actions. The plugins that are recommended are:

  • python
  • pylance
  • Prettier
  • autoDocstring

Additionally, the following user settings can be added to enable auto-formatting of code upon saving to PEP8 compliance using "black" (a requirement of all BHoM Python code).

{
  "python.formatting.provider": "black",
  "python.defaultInterpreterPath": "C:/ProgramData/BHoM/Extensions/PythonEnvironments/Example_Toolkit/python.exe",
  "editor.formatOnType": true,
  "editor.formatOnSave": true,
  "editor.codeActionsOnSave": {
    "source.fixAll": true,
    "source.organizeImports": true,
    "source.sortMembers": true
  },
  "python.formatting.blackPath": "C:/ProgramData/BHoM/Extensions/PythonEnvironments/Python_Toolkit/Scripts/black.exe",
  "python.linting.pylintArgs": ["--disable=C0114"]
}

Scripting/notebooks

Jupyter notebooks provide a good testing environment for Python code development. The registered ipykernel associated with each BHoM Python environment enables creation of environment specific notebooks with access to all code contained therein. Once you've got the base BHoM Python environment installed, you can run a notebook server using

C:/ProgramData/BHoM/Extensions/PythonEnvironments/Python_Toolkit/python.exe -m jupyter lab

During development it can be useful to reload changes in code rather than restarting the kernel each time you modify the source code. To do this, include the following in the first cell in a notebook and changes you make in the referenced *.py files will be brought through into the notebook environment.

%load_ext autoreload
%autoreload 2