Showing posts with label Python. Show all posts
Showing posts with label Python. Show all posts

Tuesday, March 17, 2026

PyBind Setup Cheat Sheet

In 2024, we checked out OpenAI Retro Cheat Sheet as an open-source project that provides an interface to interact with various retro video games for purpose of Reinforcement Learning research. This project: using C/C++ for high-performance but leverages pybind11 to create bindings for Python client code consumption.

Let's check it out!

pybind11
A lightweight header-only library that can be used to integrate C++ with Python to create bindings exposing C++ functions to Python. Client code written in Python can be consumed to invoke the underlying C++ code.

Installation
All examples here are executed on Ubuntu Linux. Therefore install pybind11 globally to begin the examples:
 sudo apt-get update
 sudo apt-get install pybind11-dev
 sudo apt install build-essential g++

Example I
Create an example that exposes C++ function to Python with pybind11. Launch terminal | Enter commands:
  mkdir -p ~/HelloPyBind
  cd HelloPyBind
  python  -m venv .venv
  source .venv/bin/activate           # OR .\.venv\Scripts\activate
  which python
  `which python` --version            # Python 3.8.10
  pip install pybind11
  pip install --upgrade pip


Create the following files: example.cpp, setup.py, test.py. Enter the following C++ and Python source code:
  example.cpp
  #include <pybind11/pybind11.h>
  
  int add(int x, int y)
  {
      return x + y;
  }
  
  PYBIND11_MODULE(example, m)
  {
      // optional module docstring
      m.doc() = "pybind11 example plugin";
      m.def("add", &add, "A function which adds two numbers");
  }

  setup.py
  from setuptools import setup, Extension
  import pybind11
  
  ext_modules = [
      Extension(
          "example",
          ["example.cpp"],
          include_dirs=[pybind11.get_include()],
          language="c++"
      ),
  ]
  
  setup(
      name="example",
      version="0.1",
      ext_modules=ext_modules,
  )

  test.py
  import example
  
  result = example.add(1, 2)
  print(f"1 + 2 = {result}")

Build C++ code using setup.py build inplace. Finally execute python test.py for Python to execute C++ code!
  python setup.py build_ext --inplace		# example.cpython-38-x86_64-linux-gnu.so
  python test.py				# OUTPUT	1 + 2 = 3


Example II
Repeat previous exercise but prefer PyCharm IDE. Launch PyCharm | New Project. Enter the following info:

 Location: ~/HelloPyBind
 Interpreter type:  uv
 Python version: 3.11
 Path to uv:  ~/.local/bin/uv

PyCharm should setup UV virtual environment and configure Python interpreter if not then enter commands:
  uv venv --python 3.11
  source .venv/bin/activate           # OR .\.venv\Scripts\activate
  which python
  `which python` --version            # Python 3.11.11

In the PyCharm Terminal | Enter the following commands for UV to install and sync package dependencies:
  uv add pybind11
  uv add setuptools
  uv lock
  uv sync

Create the following files: example.cpp, setup.py, test.py. Enter C++ and Python code similar to Example I. Build C++ code using setup.py build install. Finally execute uv run test.py for Python to execute C++ code!
  uv run setup.py build		
  uv run setup.py install		# example.cpython-38-x86_64-linux-gnu.so
  uv run test.py			# OUTPUT	3 + 5 = 8	9 - 5 = 4


Example III
Repeat previous exercise but prefer CMake to build C++ code via CMakeLists.txt. Create PyCharm Project:
 Location: ~/HelloPyBind
 Interpreter type:  uv
 Python version: 3.11
 Path to uv:  ~/.local/bin/uv

In the PyCharm Terminal | Enter the following commands for UV to install and sync package dependencies:
  uv add pybind11
  uv sync

Create the following files: example.cpp, CMakeLists.txt, test.py. Enter code similar to Example II but update:
  CMakeLists.txt
  cmake_minimum_required(VERSION 3.16)
  project(example)
  
  # Find the Python 3.11-specific pybind11 CONFIG from pip
  execute_process(
          COMMAND ${Python3_EXECUTABLE} -m pybind11 --cmakedir
          OUTPUT_VARIABLE pybind11_DIR
          OUTPUT_STRIP_TRAILING_WHITESPACE
  )
  find_package(pybind11 REQUIRED CONFIG)
  pybind11_add_module(example example.cpp)

Build C++ code using cmake and make. In the PyCharm Terminal | Enter the following commands to build:
  mkdir -p build
  cd build
  cmake -DPython3_EXECUTABLE=$(which python) ..
  make -j$(grep -c ^processor /proc/cpuinfo)

Enter commands to copy library to be used. Finally execute uv run test.py for Python to execute C++ code!
  python -c "import sysconfig, shutil, glob;		\		
  dst = sysconfig.get_paths()['platlib'];		\
  so = glob.glob('*.so')[0];				\
  shutil.copy2(so, dst)"
  cd ..
  uv run test.py			# OUTPUT	Hello, World!

IMPORTANT
The first 3x examples worked but tightly coupled Python and C++ without being able to debug separately!

Example IV
Repeat previous exercise but prefer to modify the project layout to separate top level Python and C++ code:
  ~/HelloPyBind/
  ├── cpp/
  │   ├── src/
  │   │   ├── api/
  │   │   │   ├── my_api.h
  │   │   │   └── my_api.cpp
  │   │   ├── bindings/
  │   │   │   └── pybind_module.cpp       # pybind11 bindings
  │   │   ├── CMakeLists.txt
  │   │   └── main.cpp                    # C++ executable entry point
  │   ├── tests/
  │   │   ├── CMakeLists.txt
  │   │   └── test_api.cpp
  │   └── CMakeLists.txt                  # top-level C++ (CLion entry point)
  │
  └── python/
      ├── .venv/
      │   └── lib/
      │       └── python3.11/
      │           └── site-packages/
      │               └── my_api_py.cpython-311-x86_64-linux-gnu.so
      ├── test.py
      ├── pyproject.toml
      └── README.md

Create PyCharm Project. Setup virtual environment as before then create CLion project to build C++ code.
 Location: ~/HelloPyBind/python
 Interpreter type:  uv
 Python version: 3.11
 Path to uv:  ~/.local/bin/uv

Launch CLion | New Project. Create C++ Executable using C++ 17. Enter the following CLion information:
 C++ C++ Executable
 Location: ~/HelloPyBind/cpp
 Language standard: C++17

Set build directory in CLion. File menu | Settings... | Build, Execution, Deployment | CMake | Build directory

Setup folder layout as above. Enter all C++ source code and tests. Rebuild entire solution in Debug mode.

IMPORTANT
CMakeLists.txt files are configured to copy shared object SO file into the Python .venv virtual environment

Launch PyCharm | Complete the test runner. Finally execute uv run test.py for Python to execute C++ code!
  uv run test.py				# OUTPUT	1 + 2 = 3


Example V
Repeat previous exercise but prefer more complexity to build C++ code with classes as consumed by Python

Create PyCharm Project. Setup virtual environment as before then create CLion project to build C++ code. Launch CLion | New Project. Create C++ Executable using C++ 17. Enter the following CLion from before.

Set build directory in CLion. File menu | Settings... | Build, Execution, Deployment | CMake | Build directory. Setup folder layout as above. Enter all C++ source code and tests. Rebuild entire solution in Debug mode.

Launch PyCharm | Complete the test runner. Finally execute uv run test.py for Python to execute C++ code!
  uv run test.py		# OUTPUT	
  # Guitar: 'Fender' [6-string] = $1500.0
  # Guitar: 'Ibanez' [7-string] = $1200.0
  # Guitar: 'Gibson' [6-string] = $2400.0


Example VI
Repeat previous exercise but prefer more complexity to build C++ code with templates consumed by Python

Create PyCharm Project. Setup virtual environment as before then create CLion project to build C++ code. Launch CLion | New Project. Create C++ Executable using C++ 17. Enter the following CLion from before.

Set build directory in CLion. File menu | Settings... | Build, Execution, Deployment | CMake | Build directory. Setup folder layout as above. Enter all C++ source code and tests. Rebuild entire solution in Debug mode.

Launch PyCharm | Complete the test runner. Finally execute uv run test.py for Python to execute C++ code!
  # OUTPUT	
  # Container[0] = 0.0
  # Container[1] = 1.0
  # Container[2] = 2.0
  # Container[3] = 3.0
  # Container[4] = 4.0
  # OUTPUT	
  # Container[5] = 5.0
  # Container[6] = 6.0
  # Container[7] = 7.0
  # Container[8] = 8.0
  # Container[9] = 9.0


Example VII
Repeat previous exercise but prefer Visual Studio 2022 on Windows to build an increasing C++ code base:
  ~/HelloPyBind/
  ├── cpp/
  │   ├── src/
  │   │   ├── core/			  # Core API implementation
  │   │   │   ├── *.h
  │   │   │   └── *.cpp
  │   │   ├── math/			  # Math-related API
  │   │   │   ├── *.h
  │   │   │   └── *.cpp
  │   │   ├── mesh/			  # Mesh-related API
  │   │   │   ├── *.h
  │   │   │   └── *.cpp
  │   │   ├── bindings/
  │   │   │   └── pybind_module.cpp       # pybind11 bindings
  │   │   ├── CMakeLists.txt
  │   │   └── main.cpp                    # C++ executable entry point
  │   ├── tests/
  │   │   ├── CMakeLists.txt
  │   │   ├── test_matrix.cpp
  │   │   ├── test_vector.cpp
  │   │   ├── test_mesh.cpp
  │   │   ├── test_mesh_algorithms.cpp
  │   │   └── test_mesh_processor.cpp
  │   └── CMakeLists.txt                  # top-level C++ (CLion entry point)
  └── python/

Launch Visual Studio 2022 | Continue without code. File | Open | CMake... Navigate to cpp/CMakeLists.txt. Build menu | Build All. Finally, choose Test menu | Test Explorer. Run All Tests in View or choose to Debug:


Summary
To summarize, we have demonstrated various PyBind examples in which C++ library code is consumed by a single Python API exclusively. However, in future there may be instances in which the C++ library may need to be consumed by multiple languages. In this case ctypes.cdll.LoadLibrary() may be better that PyBind!

Tuesday, February 3, 2026

Python Package Cheat Sheet

In 2020, we checked out Python Setup Cheat Sheet as an interpreted high-level programming language runs on Windows, Mac OS/X and Linux using pip as the de facto standard package-management system. However while this worked, requirements.txt is brittle with package dependencies + versioning. Poetry was introduced to resolve deterministic builds but at slower dependency resolution. Enter uv for blazing speed + reliability J

Let's check it out!

History
Python Setup Cheat Sheet detailed how to install pip as the de facto standard package-management system on Windows, Mac OS/X and Linux. However, requirements.txt does not lock down transitive dependencies + versioning which becomes brittle. Poetry solved this problem using pyproject.toml configuration and lock file.

uv
Replicating Poetry with more deterministic builds using using pyproject.toml configuration and lock file, uv is built in Rust by Astral as a full rethinking of Python packaging designed for speed and simplicity. uv: pitched as "A single tool to replace pip, pip-tools, pipx, poetry, pyenv, twine, virtualenv and more". Here is a detailed article comparing the Python Packaging Landscape: Pip vs. Poetry vs. UV from a developer's standpoint.

IMPORTANT
First check out the traditional way using python and pip to compare and illustrate benefits of now using uv:

Virtual Environment
A virtual environment isolates Python project interpreter and installed packages from the system and other Python projects which means each project should have its own environment, its version and dependencies.

Create a virtual environment in the traditional way using python and pip then activate virtual environment:
 python -m venv .venv
 Linux OR Mac OS/X  source .venv/bin/activate
 Windows  .\.venv\Scripts\activate

Next, install packages using python, pip and requirements.txt OR poetry with pyproject.toml configuration:
Brittle and/or slow [transitive] dependency resolution!

Installation
Download and install uv for Linux, Mac OS/X or Windows OR Launch PyCharm and install from home page:
 Linux  curl -LsSf https://astral.sh/uv/install.sh | sh
 Mac OS/X   brew update && brew install uv
 Windows   powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"

uv init
Create new directory and execute following commands to initialize Python project with an interpreter version

Launch Terminal | Execute the following commands:
  mkdir HelloUV
  cd HelloUV
  uv init --python 3.11.11

uv venv
Navigate into Python project and execute following commands to create the virtual environment and activate

Launch Terminal | Execute the following commands:
  uv venv --python 3.11.11
  source .venv/bin/activate		# Windows: .\.venv\Scripts\activate

uv python
At this point you have Python project initialized with virtual environment activated. Confirm correct version of Python interpreter installed and activate. When using PyCharm ensure the IDE interpreter path is aligned!

Inside PyCharm Terminal | Execute the following commands:
  which python
  `which python` --version

uv add
Install Python packages either using uv pip install or uv add commands. Prefer uv add because this updates pyproject.toml file automagically thus you are able to execute uv sync command to install the dependencies.

Inside PyCharm Terminal | Execute the following commands:
  uv pip list
  uv add requests
  uv sync

uv tree
After execute uv add you can verify what is installed by checking pyproject.toml file or execute uv pip list but another useful method is uv tree which shows hierarchy of all your project's dependencies and relationships.

Inside PyCharm Terminal | Execute the following command: uv tree

uv sync
Execute uv add or update pyproject.toml to install dependencies. Execute uv sync to update environment.

uv lock
The uv.lock file records all the exact versions of all project dependencies UV figures are compatible from the pyproject.toml file to ensure constant deterministic builds with the same dependenices each time and CI/CD

uv tool
UV tool installs persistent tools into virtual environment that are required for that particular Python project:

Inside PyCharm Terminal | Execute the following commands:
  uv tool list
  uv tool install ruff
  uv tool run ruff check
  uv tool upgrade --all
  uv tool uninstall ruff
  uv tool list

uvx tool
Finally uvx is an alias for uv tool run but is designed to be run immediately without installing it persistently:

Inside PyCharm Terminal | Execute the following command: uvx ruff check

uv cache
When you use uv all tools and dependencies are stored in cache. These commands remove all cached data:
  uv cache clean
  rm -r "$(uv python dir)"
  rm -r "$(uv tool dir)"

Commands
Here is a quick summary of popular uv commands used during workflow. A comprehensive list can be found.
  uv init --app				# Scaffold project
  uv python install 3.11		# Install Python
  uv venv --python 3.11			# Create virtual environment
  uv add requests			# Add dependencies
  uv add -D pytest			# Add dev dependencies
  uv sync --frozen			# Sync (locked)
  uv sync				# Sync (normal)
  uv run python main.py			# Run program
  uv run python -V			# Show Python
  uvx ruff check .			# Run tools ad‑hoc
  uv lock				# Update lockfile

Docker
A well-built uv Docker image simplifies deployment + ensures application runs consistently in environment:
  FROM python:3.11.11-slim
  # Install uv
  COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
  COPY . /app
  WORKDIR /app
  # Install dependencies and clear cache
  RUN uv sync --no-dev --frozen
  RUN rm -rf ~/.cache/uv
  CMD ["python", "-c", "print('Hello, World!')"]

This is an example Dockerfile snippet how to integrate uv! But build and run execution with the commands:
  docker build -t uv-hello .
  docker run --rm uv-hello

GitHub Actions
A common GitHub Actions pattern is to use UV to install only Prod dependencies during your build or deploy:
  name: Testing
  on:
    push:
      branches:
        - "main"
  jobs:
    build:
      runs-on: ubuntu-latest
      steps:
        - uses: actions/checkout@v4
        - name: Install UV
          run: curl -LsSf https://astral.sh/uv/install.sh | sh
        - name: Sync only prod dependencies
          run: uv sync --no-group dev --group prod
        - name: Run tests
          run: uv run pytest

Summary
To summarize, uv has been pitched "One Tool for Everything" as uv replaces pip, virtualenv, pip-tools, pipx, poetry, pyenv, twine and is fast at every step. However, this has only scratched the surface as uv integrates well with other tools for fast reproducible Python workflows such as direnv, pre-commit hooks, pdm + more!
 uv  Handles Python installs, environment creation, and dependency resolution with locking
 direnv   Evaluates .envrc managing environment variables automatically applied to shell session
 pdm  Orchestrate workflows + provide unified tooling to build, version, and publish pacakges

Monday, September 15, 2025

Pytest Setup Cheat Sheet

In 2020, we checked out Python Setup Cheat Sheet as an interpreted high-level programming language with all code samples' unit tests using unittest package TestCase class. However, since then we have learned that pytest allows writing shorter more readable tests with less boilerplate. Plus we would like to include mocks!!

Let's check it out!

Frameworks
When developing code in Python there are typically five Top Python Testing Frameworks that are favorable:
 NAME  MONIKER   DESCRIPTION
 unittest  PyUnit  The default Python testing framework built-in with the Python Standard Library
 pytest  Pytest  Popular testing frameworks known for simplicity, flexibility + powerful features
 noseTest  Nose2  Enhanced unittest version offering additional plugins to support test execution
 DocTest  DocTest  Python Standard Library module generates tests within source code DocString
 Robot  Robot  Acceptance testing keyword-driven module that simplifies testcase automation

Here are some reasons why pytest currently seems to be the most popular Python unit test framework out:
  1. Simple and Readable Syntax
     You write plain Python functions instead of creating large verbose classes
     Assertions use plain assert statements which provide more detailed output
  2. Rich Plugin Ecosystem
     Plugins like pytest-mock, pytest-asyncio, pytest-cov, and more
     Easy to extend pytest plugins or write your own custom plugins
  3. Powerful Fixtures
     Allows for clean and re-usable setup and teardown using fixtures
     Supports various test level scopes, autouse, and parametrization
  4. Test Discovery
     Automatically discovers tests in files named test_*.py
     No need to manually register tests or use loader classes
  5. Great Reporting
     Colored output, diffs for failing assertions, and optional verbosity
     Integrates easily with tools like coverage, tox, and CI/CD systems
  6. Supports Complex Testing Needs
     Parameterized tests (@pytest.mark.parametrize)
     parallel test execution (pytest-xdist) + hooks

pytest
  pip install pytest

Setup
Depending on your stack here is some great documentation to setup pytest on PyCharm, VS Code or Poetry.

Configuration
In pytest, pytest.ini is the main configuration file used to customize and control pytest behavior across the unit test suite. pytest.ini hosts pytest options, test paths, plugin settings and markers to attach to the test functions to categorize, filter or modify their behavior. Here is a sample pytest.ini configuration file as base:
  [pytest]  
  addopts = -ra -q
  testpaths = tests
  markers =
      slow: marks tests as slow (deselect with '-m "not slow"')
      db: marks tests requiring database

Fixtures
Fixtures are methods in pytest that provide fixed baseline for tests to run. Fixtures can be used to setup all preconditions for tests, provide data, or perform teardown after tests finished via @pytest.fixture decorator.

Scope
Fixtures have scope: Function, Class, Module + Session which define how long fixture available during test:
 SCOPE DESCRIPTION
 Function Fixture created once per test function and destroyed at end of test function
 Class Fixture created once per test class and destroyed at the end of test class
 Module Fixture created once per test module and destroyed at end of test module
 Session Fixture created once per test session and destroyed at end of test session

conftest
In pytest, conftest.py file is used to share fixtures across multiple tests. All the fixtures in conftest.py will be automagically detected without needing to import. conftest: typically scoped at test root directory structure.

Dependencies
Dependency Injection: when fixtures are requested by other fixtures although this adds complexity to tests!

autouse
Simple trick to avoid defining fixture in each test: use the autouse=True flag to apply fixture to all tests.

yield
When you use yield in fixture function setup code executes before yield and teardown executes after yield:
  import pytest  
  @pytest.fixture
  def my_fixture(): 
      # setup code
      yield "fixture value"
      # teardown code

Arguments
Use pytest fixtures with arguments to write re-usable fixtures that can easily share across tests also known as Parameterized fixtures using @pytest.fixture(params=[0, 1, 2]) syntax. Note: these fixtures should not be confused with the @pytest.mark.parametrize decorator which can be used to specify inputs and outputs!

Factories
Factories, in the context of pytest fixtures, are functions that are used to create and return instances of objects that are needed to generate test data or objects with specific configuration in re-usable manner:
 conftest.py  unittest.py
 @pytest.fixture
 def user_creds(): 
   def _user_creds(name: str, email: str):
     return {"name": name, "email": email}  
   return _user_creds
 def test_user_creds(user_creds):
   assert user_creds("John", "x@abc.com")=={  
     "name": "John",  
     "email": "x@abc.com",
   }

Best practices for organizing tests include: Organizing Tests by Testing Pyramid, Structure Should Mirror Application Code, Group or Organize Fixtures and Organize Tests Outside Application Code for scalability.

Mocking
Mocking is technique that allows you to isolate pieces of code being tested from its dependencies so the test can focus on the code under test in isolation. The unittest.mock package offers Mock and MagicMock objects:

Mock
A mock object simulates the behavior of the object it replaces by creating attributes and methods on-the-fly.

MagicMock
Subclass of Mock with default implementations for most magic methods (__len__, __getitem__, etc.). Useful when mocking objects that interact with Python's dunder methods that enable custom behaviors for common operations.

Patching
Patching is technique that temporarily replaces real objects in code with mock objects during test execution. Patching helps ensure external systems do not affect test outcomes thus tests are consistent and repeatable.

IMPORTANT - Mocks are NOT stubs!
When we combine @patch decorator with return_value or side_effect it is a stub but from the mock package!
 METHOD DESCRIPTION
 return_value Specify the single value of Mock object to be returned when method called
 side_effect Specify multiple values of Mock object to be returned when method called

Difference
In pytest, Mock and patch are both tools for simulating or replacing parts of your code during testing. Mock creates mock objects while patch temporarily replaces real objects with mocks during tests to isolate code:
 Mock  patch
  from unittest.mock import Mock  
  
  mock_obj = Mock()
  mock_obj.some_method.return_value = 42 
  result = mock_obj.some_method()  
  assert result == 42
  from unittest.mock import patch
  
  def external_function(): 
      pass
  
  @patch('module_name.external_function')  
  def test_function(mock_external): 
      mock_external.return_value = "Mock data"
      result = external_function() 
      assert result == "Mock data"
IMPORTANT
When creating mocks it is critical to ensure mock objects accurately reflect objects they are replacing. Thus, it is best practice to use autospec=True to ensure mock objects respect function signatures being replaced!

Assertions
For completeness, here is list of assertion methods to verify method on mock object was called during tests:
 METHOD DESCRIPTION
 assert_called verify specific method on mock object has been called during a test
 assert_called_once verify specific method on mock object has been called only one time
 assert_called_once_with verify specific method on mock object called once with specific args
 assert_called_with verify every time method on mock object called with fixed arguments
 assert_not_called verify specific method on mock object was not called during the test
 assert_has_calls verify the order in which specific method on mock object was called
 assert_any_call verify specific method on mock object has been called at least once

Monkeypatch
Monkeypatching is technique used to modify code behavior at runtime especially where certain dependencies or settings make it challenging to isolate functionality for example environment variables or system paths:
  app.py   test_app.py
  import os
  def get_app_mode() -> str:
      app_mode = os.getenv("APP_MODE") 
      return app_mode.lower()
  def test_get_app_mode(monkeypatch):
      """Test behavior when APP_MODE is set."""
      monkeypatch.setenv("APP_MODE", "Testing") 
      assert get_app_mode() == "testing"

pytest-mock
pytest-mock is pytest plugin built on top of unittest.mock that provides an easy-to-use mocker fixture that can be used to create mock objects and patch functions. When you use mocker.patch() method provided by pytest-mock default behavior is to replace the object with MagicMock() so pytest-mock uses MagicMock().
  pip install pytest-mock

  app.py
  import requests
  from http import HTTPStatus
  
  def get_user_name(user_id: int) -> str:
      response = requests.get(f"https://api.example.com/users/{user_id}")
      return response.json()['name'] if response.status_code == HTTPStatus.OK else None

  test_app.py
  from http import HTTPStatus
  from app import get_user_name
  
  def test_get_user_name(mocker):
      mock_response = mocker.Mock()
      mock_response.status_code = http.HTTPStatus.OK
      mock_response.json.return_value = {'name': 'Test'}
      mocker.patch('app.requests.get', return_value=mock_response)
      result = get_user_name(1)
      assert result == 'Test'

Legacy
In many legacy Python codebases you may detect references to Mock(), MagicMock() and @patch decorator from unittest.mock with pytest. Teams often keep the old style unless there compelling reason to refactor it.

Recommendation
However, here are some recommendations to prefer pytest-mock and mocker fixture for future unit testing:
  1. Prefer pytest-mock and the mocker fixture
     Cleaner syntax than unittest.mock.patch
     Automatically cleaned up after each test
     Plays well with other pytest fixtures
     Centralizes all patching into one fixture (mocker)
  2. Use monkeypatch for patching env vars, system paths and etc.
     Prefer monkeypatch for clarity and idiomatic pytest style
     e.g. os.environ, system paths, or patching open()
  3. Avoid @patch decorators unless migrating old tests
     Can be harder to read or stack with multiple patches
     Better to use mocker.patch() inline as cleaner syntax
  4. Use autospec=True when mocking complex or external APIs
     Ensure mocks behave like the real objects (catch bad call signatures)
  5. Use fixtures to share mocks across tests
     When you have mock used by multiple tests then define it as a fixture
tl;dr
Prefer pytest-mock (mocker fixture) for readability and less boilerplate. Import tools like MagicMock, Mock, call, ANY from unittest.mock when needed. Avoid @patch unless needed — inline mocker.patch() is usually cleaner. Keep everything in one style within a test module for consistency.

pytest-asyncio
Concurrency allows a program to efficiently execute its tasks asynchronously i.e. executing tasks while other tasks are waiting. pytest-asyncio simplifies handling event loops + managing async fixtures thru unit testing.
  pip install pytest-asyncio

  app.py   test_app.py
  import asyncio
  
  
  async def fetch_data():
      # Simulate I/O operation.
      await asyncio.sleep(1)
      return {"status": "OK", "data": [42]} 
  import pytest
  from app import fetch_data
  
  @pytest.mark.asyncio
  async def test_fetch_data():
      result = await fetch_data()
      assert result["status"] == "OK" 
      assert result["data"] == [42]
Consequently AsyncMock from unittest.mock allows you to mock asynchronous functions and/or coroutines.

CI/CD
GitHub Actions is feature-rich CI/CD platform and offers an easy and flexible way to automate your testing processes. GitHub Actions mainly consist of files called workflows. The workflow file contains job or several jobs that consist of sequence of steps. Here is sample YAML file that will trigger the workflow on git push:
  ~/.github/workflows/run_test.yml
  name: Run Unit Test via Pytest
  on: [push]
  jobs:
    build:
      runs-on: ubuntu-latest
      strategy:
        matrix:
          python-version: ["3.10"]
      steps:
        - uses: actions/checkout@v3
        - name: Set up Python ${{ matrix.python-version }}
          uses: actions/setup-python@v4
          with:
            python-version: ${{ matrix.python-version }}
        - name: Install dependencies
          run: |
            python -m pip install --upgrade pip
            if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
        - name: Lint with Ruff
          run: |
            pip install ruff
            ruff --format=github --target-version=py310 .
          continue-on-error: true
        - name: Test with pytest
          run: |
            coverage run -m pytest  -v -s
        - name: Generate Coverage Report
          run: |
            coverage report -m

Summary
To summarize, we have setup pytest for more robust unit testing with mocks and stubs via patching. Looking forward there are additional ways to improve unit test development experience with pytest as per the article:
  1. Use Markers To Prioritise Tests
     Organize tests in such a way that prioritizes key functionalities first
     Running tests with critical functionality first provide faster feedback
  2. Do More With Less (Parametrized Testing)
     Parametrized Testing allows you to test multiple scenarios in single test function
     Feed different parameters into same test logic covering more scenarios + less code
  3. Profiling Tests
     Identify the slow-running unit tests using the --durations=XXX flag
     Use the pytest-profiling plugin to generate tabular and heat graphs
  4. Run Tests In Parallel (Use pytest-xdist)
     Use the pytest-xdist plugin to distribute tests across multiple CPUs
     Tests run in parallel, use resources better, provide faster feedback!

Monday, August 31, 2020

Python Setup Cheat Sheet II

In the previous post we checked out Python Setup Cheat Sheet to download and install Python and setup an integrated development environment for Python programming. We installed open source Python distribution Anaconda used for data science. Let's continue setup to build artificial intelligence + machine learning apps.

Let's check it out!


Install VS Code
VS Code is an integrated development environment with useful plugins for Python and Data Science. Install VS Code from Anaconda navigator. Otherwise install VS Code for Windows and Mac OS/X and Linux directly.

Launch VS Code. Install Python extension for Visual Studio Code. Install other plugins for example Remote SSH to connect to Linux VM or Code Runner. Install VS Code Insiders to try pre-release version of VS Code.

Update global settings.json file to your particular preferences. Here are some common VS Code examples:
 SYSTEM  LOCATION
 Windows  %APPDATA%/Code/User/settings.json
 Mac OS/X  ~/Library/Application Support/Code/User/settings.json
 Linux  ~/.config/Code/User/settings.json
Note: VS Code Insiders location replace Code/User/settings.json with Code - Insiders/User/settings.json

settings.json
 {
    "workbench.colorTheme": "Visual Studio Light",
    "window.zoomLevel": 0,
    "editor.trimAutoWhitespace": false,
    "editor.renderIndentGuides": false,
    "editor.roundedSelection": false,
    "editor.suggestSelection": "first",
    "python.dataScience.sendSelectionToInteractiveWindow": true,
    "python.jediEnabled": false,
    "code-runner.clearPreviousOutput": true,
    "code-runner.runInTerminal": true,
    "code-runner.showExecutionMessage": false,
    "code-runner.respectShebang": false,
    "code-runner.defaultLanguage": "python3",
 }

 Windows  Mac OS/X + Linux
 {
    "python.pythonPath": "%USERPROFILE%/Anaconda3/python.exe",
    "code-runner.executorMap": {
        "python": "python.exe",
        "python.pythonPath": "%USERPROFILE%/Anaconda3",
    },
 }
 {
    "python.pythonPath": "/anaconda3/bin/python",
    "code-runner.executorMap": {
        "python": "python",
        "python.pythonPath": "/anaconda3/bin",
    },
 }


Hello VS Code
Create folder "HelloVScode". Launch VS Code. File | Open Folder | HelloVScode | Select Folder. Create simple "HelloWorld.py" file. Enter simple code print('Hello World'). Press F5 to debug Python script. Customize Run + Debug click "create a launch.json file". Click Python File: Debug the currently active Python file. Press F5.

IMPORTANT
Press Ctrl + Shift + P | Python: Select Interpreter. If creates new workspace settings.json + override Python interpreter user settings.json then do not check python.pythonPath into source control when deploying cross platform.

Virtual Environment
Unlike PyCharm, VS Code does not automagically create an isolated Python environment for new projects. Therefore, follow all instructions here to setup and activate virtual environment for Python using VS Code.

CommandNotFoundError
When running Python script using Anaconda you may get error CommandNotFoundError: Your shell has not been properly configured to use 'conda activate'. Terminal | Click "+" | Spawn new Terminal default "Conda".

Code Runner
Automate process by installing Code Runner plugin. Press F1 | Run Code first time to set Output to "Code". Ensure entries above in user settings.json for fast turnaround. Press Ctrl + Alt + N for each subsequent run!

IMPORTANT
On Mac OS/X if Ctrl + Alt + N does not work then you may have to swap Ctrl for Cmd in keybindings.json:
~/Library/Application Support/Code/User/keybindings.json
// Place your key bindings in this file to override the defaultsauto[]
[
    {
        "key": "cmd+alt+n",
        "command": "code-runner.run"
    },
    {
        "key": "cmd+alt+n",
        "command": "-code-runner.run"
    }
]


Code Sample
Let's test drive develop a simple code sample as a Python module that could be deployed as Python package.

IMPORTANT
A module is a single Python script file whereas a package is a collection of modules. A package is a directory of Python modules containing an additional __init__.py file to distinguish from a directory of Python scripts.

Create folder "PackageVScode". Launch VS Code. File | Open Folder | PackageVScode. Create New Folder "MyPackage". Create other top level folders, for example, Build + Docs etc. Create hidden .vscode folder.

Create sub folders src and tests beneath "MyPackage". Create requirements.txt file + setup.py beneath "MyPackage" also. Finally, create module.py and __init__.py under src and test_module.py under tests.

 test_module.py  module.py
 import unittest
 from MyPackage.src.module import add_one

 class TestSimple(unittest.TestCase):

    def test_add_one(self):
        result = add_one(5)
        self.assertEqual(result, 6)

 if __name__ == '__main__':
    unittest.main()
 def add_one(number):
    return number + 1

Add launch.json to .vscode folder. Accept default to launch current Python file from integrated terminal:
launch.json
 {
    "version": "0.2.0",
    "configurations": [
        {
            "name": "Python: Current File",
            "type": "python",
            "request": "launch",
            "program": "${file}",
            "console": "integratedTerminal"
        }
    ]
 }

Open test_module.py. Press F5 to debug. Output ModuleNotFoundError: No module named 'MyPackage'. In VS Code you must correctly set PYTHONPATH in order to debug step through and run local source code!

Add settings.json to .vscode folder. Configure Python source folders by configuring the env PYTHONPATH:
settings.json
 {
    "terminal.integrated.env.windows": {
        "PYTHONPATH": "${env:PYTHONPATH};${workspaceFolder}"
    },
    "terminal.integrated.env.osx": {
        "PYTHONPATH": "${env:PYTHONPATH}:${workspaceFolder}",
    },
    "terminal.integrated.env.linux": {
        "PYTHONPATH": "${env:PYTHONPATH}:${workspaceFolder}",
    },
    "python.envFile": "${workspaceFolder}/.env"
    ]
 }

Finally, you must add "hidden".env file beneath "PackageVScode" that sets workspace folder per platform:
 WORKSPACE_FOLDER=/Absolute/Path/to/PackageVScode/
 PYTHONPATH=${WORKSPACE_FOLDER}

IMPORTANT
The "hidden".env file "lives" locally to project and should NOT be checked into source code version control! It may also be necessary to close VS Code and re-open after configuring .env file. Windows must use "/".

Now press F5 to debug test code or press Ctrl + Alt + N for Code Runner. Alternatively, Terminal command:

python -m unittest discover MyPackage

IMPORTANT
If the Terminal reveals Run 0 tests then ensure package has __init__.py setup in every relevant sub folder. Finally, enter dependencies for requirements.txt file and setup.py e.g. numpy. Install package at terminal:
 setup.py  requirements.txt
 import setuptools

 with open("README.md", "r") as fh:
    LONG_DESCRIPTION = fh.read()

 setuptools.setup(
    name='MyPackage',
    version='0.1.2',
    description="My test package.",
    long_description=LONG_DESCRIPTION,
    long_description_content_type="text/markdown",
    packages=setuptools.find_packages(),
    install_requires=[
        "numpy>=1.17.1",
    ]
 )
 numpy>=1.17.1
pip install MyPackage/.

Finally, we could also replicate the unit test code directly on the REPL. Select Terminal tab. Enter commands:

 python
 >>> from MyPackage.src.module import add_one
 >>> result = add_one(5)
 >>> print(result)

Code Linting
Linting highlights syntactical and stylistic problems in Python source code which helps identify and correct subtle programming errors. In VS Code, navigate to Python script via Terminal e.g. Pylint test_module.py.

Alternatively, select Terminal and type specific linter like flake8 to check Python source code against PEP8 coding style programming errors. If flake8 is not installed then simply type pip install flake8 at Terminal.


Jupyter Notebooks
Notebooks are becoming the standard for prototyping and analysis for data scientists. Many cloud providers and Anaconda navigator offer machine learning and deep learning services in the form of Jupyter notebooks.

Launch Anaconda navigator | Choose Jupyter Notebook | Launch. After the browser launches create a New | Python 3 | Jupyter notebook. Follow tutorial. Change browser from Google Chrome to Firefox if any issues! Alternatively, create notebook in VS Code | Ctrl + Shift + P | Python: Create New Blank Jupyter Notebook.


Remote SSH
Often machine learning AI projects may require remote development in VS Code to use Windows to develop in a Linux-based environment. Install Remote SSH to run and debug Linux-based applications on Windows. Start | run | cmd. ssh username@linux_server. Enter passphrase. Enter verification code if setup for MFA.

SSH Keys
Use SSH key authentication and setup SSH keys to connect local Windows host and remote Linux VM server.
 Windows  Linux
 cd %USERPROFILE%
 cd .ssh
 ssh-keygen -C "username@emailaddress.com"
 ssh username@linux_server
 cd ~/.ssh
 ssh-keygen -C "username@emailaddress.com"

Dump the contents of id_rsa.pub file from local Windows host to authorized_keys file on Linux VM server:
cd ~/.ssh
echo "contents_of_Windows_id_rsa.pub_file" >> authorized_keys

Finally, configure %USERPROFILE%\.ssh\config file with Linux VM server information to alias connection.
 Host ENVIRONMENT
     HostName linux_server
     User username
     IdentityFile ~/.ssh/id_rsa

SSH Tunnel
Port forwarding via SSH Tunnel creates a secure connection between the local computer and remote machine through which services can be relayed. SSH Tunnel is useful for transmitting data over encrypted connection.
 ssh -N -L localhost:8787:LoadBalancer_External-IP:8000 username@linux_server

The same technique can be uesd to access Jupyter Notebook on Linux VM server. Launch terminal and enter:
 ssh username@linux_server
 cd ~/
 jupyter notebook --no-browser --port=8889
 ssh -N -L localhost:8889:localhost:8889 username@linux_server

WinSCP
Install WinSCP as popular SFTP client to navigate + copy files between local Windows and remote Linux VM. Launch WinSCP. Enter Host Name, Port number, User name, Password and verification code if setup for MFA.


Summary
To summarize, we have setup Python distribution Anaconda on Windows, Mac OS/X and Linux to now build artificial intelligence and machine learning apps. We are now set to develop machine learning models then deploy using Flask API. Apps can then be containerized using Docker and orchestrated using Kubernetes to significantly increase the efficiency of a Continuous Integration / Continuous Deployment infrastructure J

Saturday, July 4, 2020

Python Setup Cheat Sheet

Python is an interpreted high-level and general purpose programming language. Python 2.0 was released in 2000 but officially discontinued in 2020. Python 3.x is now the preferred version for most projects e.g. v3.7.

Python is commonly used in artificial intelligence and machine learning projects with the help of libraries like TensorFlow, Keras, Pytorch and Scikit-learn. There is also open source Python distribution Anaconda used for data science and machine learning applications that aims to simplify package management and deployment.

Let's check it out!


Install Python
Install Python on Windows, Mac OS/X and Linux. Install pip as the de facto standard package-management system. Install Anaconda for future AI projects. Finally, install + configure an IDE for Python programming.

Windows
Follow instructions here how to install Python 3 on Windows. Download latest version of Python for Windows including IDLE pip + documentation. Add Python to PATH variable making it easier to configure your system.

Mac OS/X
Python is installed by default on most Mac OS/X systems. Launch terminal + type python and pip to confirm.

Linux
Python is installed by default on most Linux systems. Launch terminal + type python. However you may also need to install pip. Update your ~/.bashrc file as necessary to use Python 3.7 by default instead of Python 2.
 Install + Update + Aliases Python3  Ubuntu Linux unable to locate package python-pip
 sudo apt install python3-pip
 python -m pip install --upgrade pip
 echo "alias python=python3" >> ~/.bashrc
 echo "alias pip=pip3" >> ~/.bashrc
 sudo apt-get install software-properties-common
 sudo apt-add-repository universe
 sudo apt-get update
 sudo apt-get install python3-pip

IMPORTANT
Verify which python version and which pip version are installed and location where these are both installed:
 python --version  which python
 pip --version  which pip


Install Anaconda
Follow instructions to install open source Python distribution Anaconda for Windows and Mac OS/X and Linux
 SYSTEM  LOCATION
 Windows  %USERPROFILE%\Anaconda3
 Mac OS/X  /anaconda3
 Linux  /anaconda3

Windows
Add the following four environment variables: System | Advanced system settings | Environment Variables:
 %USERPROFILE%\Anaconda3  %USERPROFILE%\Anaconda3\libs
 %USERPROFILE%\Anaconda3\Scripts  %USERPROFILE%\Anaconda3\Library\bin

Mac OS/X
Follow all prompts from the install wizard. Anaconda should install at /anaconda3 by default with aliases set.

Linux
Launch terminal as root user. Type bash ~/Downloads/Anaconda3-2020.02-Linux-x86_64.sh. For consistency with the Mac set install location to /anaconda3. After install launch Anaconda navigator. Enter the following:
 source ~/anaconda*/bin/activate root
 anaconda-navigator

Update your ~/.bashrc file as necessary to prefer Anaconda Python especially for data science AI + ML work.
 alias python=/anaconda3/bin/python
 alias pip=/anaconda3/bin/pip

Finally, create Anaconda desktop shortcut: create the following Anaconda.desktop at /usr/share/applications/ Add the text below. Enter sudo echo "PATH=$PATH:/anaconda3/bin" >> /etc/environment at the terminal.

Anaconda.desktop
[Desktop Entry]
Type=Application
Name=Anaconda
Exec=anaconda-navigator
Terminal=false
Icon=/anaconda3/lib/python3.7/site-packages/anaconda_navigator/static/images/anaconda-icon-256x256.png
Restart Linux. From settings type "Anaconda" to prompt Anaconda Navigator + resume projects from there.

IMPORTANT
Verify which Anaconda version is installed using conda --version command and where installed where conda.


Install PyCharm
PyCharm is an integrated development environment used specifically for Python. Install PyCharm during the Anaconda install or from navigator. Otherwise install PyCharm for Windows and Mac OS/X and Linux directly.

Launch PyCharm. Click "Configure" cog drop down | Settings. Set the base interpreter to match the Python location for Anaconda. Click cog | Show All | Click "+" | Virtualenv Environment. Enter the following details:

 SYSTEM  LOCATION
 Windows  %USERPROFILE%\Anaconda3\python.exe
 Mac OS/X  /anaconda3/bin/python
 Linux  /anaconda3/bin/python

Plugins
Configure cog | Plugins. Install pylint as a code analysis tool that follows the recommended PEP8 style guide. Restart IDE. Pylint is handy tool to check Python code especially for developers used to static code compiler!

Shortcuts
Track active item in solution explorer similar to Visual Studio: Click cog to right of Project and Always Select Opened File. Hit shift key twice for quick search. Remember Rename files and folder is in the Refactor menu!
 Ctrl + F12  Prompt popup to list all the methods in the class
 Ctrl + Shift + N  Prompt popup to search for text found in all files
 Ctrl + Shift + I  View definition of function or method in the class
 Ctrl + Click function  Navigate to function definition or method in class
 Ctrl + Alt + Left arrow  Navigate backwards to previous location
 Ctrl + Alt + Right arrow  Navigate forwards to the next location

IMPORTANT
On Linux navigate may actually be Shift + Alt + arrow keys instead of Ctrl due to keymap shortcut conflicts!


Hello PyCharm
Launch PyCharm. Create New Project | "HelloPyCharm". Enter the following details as Anaconda interpreter:

PyCharm uses virtualenv tool to create an isolated Python environment. Virtualenv creates venv folder in the current directory with Python executable files and a copy of pip to install other modules and packages.

Create simple "HelloWorld.py". Enter Python code print('Hello World'). Right click file | Run ""HelloWorld".

Module Not Found
Update script | Import module not currently installed e.g. import numpy. Run script: ModuleNotFoundError


Launch PyCharm terminal. Enter python -m pip install numpy to install module. Re-run script with success.


Alternatively, execute pip freeze to dump all required modules for project into requirements.txt and install:
 pip freeze > requirements.txt
 pip install -r requirements.txt


Code Sample
Let's test drive develop a simple code sample as a Python module that could be deployed as Python package.

IMPORTANT
A module is a single Python script file whereas a package is a collection of modules. A package is a directory of Python modules containing an additional __init__.py file to distinguish from a directory of Python scripts.

Launch PyCharm. Create New Project | "PackagePyCharm". Configure Python Anaconda interpreter above. Right click PackagePyCharm project | New | Python Package | "MyPackage". Create other top level folders.

Create sub folders src and tests beneath "MyPackage". Create requirements.txt file + setup.py beneath "MyPackage" also. Finally, create module.py and __init__.py under src and test_module.py under tests.

 test_module.py  module.py
 import unittest
 from MyPackage.src.module import add_one

 class TestSimple(unittest.TestCase):

    def test_add_one(self):
        result = add_one(5)
        self.assertEqual(result, 6)

 if __name__ == '__main__':
    unittest.main()
 def add_one(number):
    return number + 1

Right click inside test_module.py | Debug 'Unittests for test_module.py'. Alternatively, Terminal command:

python -m unittest discover MyPackage

IMPORTANT
If the Terminal reveals Run 0 tests then ensure package has __init__.py setup in every relevant sub folder. Finally, enter dependencies for requirements.txt file and setup.py e.g. numpy. Install package at terminal:
 setup.py  requirements.txt
 import setuptools

 with open("README.md", "r") as fh:
    LONG_DESCRIPTION = fh.read()

 setuptools.setup(
    name='MyPackage',
    version='0.1.2',
    description="My test package.",
    long_description=LONG_DESCRIPTION,
    long_description_content_type="text/markdown",
    packages=setuptools.find_packages(),
    install_requires=[
        "numpy>=1.17.1",
    ]
 )
 numpy>=1.17.1

pip install MyPackage/.

Finally, we could also replicate the unit test code directly on the REPL. Select Terminal tab. Enter commands:

 python
 >>> from MyPackage.src.module import add_one
 >>> result = add_one(5)
 >>> print(result)

Code Linting
Linting highlights syntactical and stylistic problems in Python source code which helps identify and correct subtle programming errors. In PyCharm, choose Pylint tab and click Play button to list any errors in code.

Alternatively, select Terminal and type specific linter like flake8 to check Python source code against PEP8 coding style programming errors. If flake8 is not installed then simply type pip install flake8 at Terminal.


Summary
To summarize, we have a simple setup for Python programming on Windows, Mac OS/X and Linux. There is much to explore e.g. Python 3.8. However, we'd like to setup Python more for AI machine learning projects. This will be topic of the next post.