Skip to content

Inspecting Built Packages#

This tutorial teaches you how to work with built conda packages:

  1. Load packages from .conda or .tar.bz2 files
  2. Inspect package metadata (name, version, dependencies)
  3. List all files contained in the package
  4. Discover and inspect embedded tests
  5. Run tests programmatically and capture results
  6. Rebuild packages to verify reproducibility

Let's get started!

import json
from pathlib import Path

from rattler_build import Package, RenderConfig, Stage0Recipe, VariantConfig

Step 1: Build a Package with Tests#

First, let's build a package that has embedded tests. We'll create a simple noarch Python package with: - A Python module - Package content checks

# Define a recipe with multiple test types
test_recipe_yaml = """
package:
  name: test-demo-package
  version: "1.0.0"

build:
  number: 0
  noarch: python
  script:
    interpreter: python
    content: |
      import os
      from pathlib import Path

      prefix = Path(os.environ["PREFIX"])

      # Create a Python module (noarch packages use site-packages directly)
      site_packages = prefix / "site-packages"
      site_packages.mkdir(parents=True, exist_ok=True)

      module_file = site_packages / "demo_module.py"
      module_file.write_text('''

      __version__ = "1.0.0"

      def greet(name: str) -> str:
          return f"Hello, {name}!"

      def add(a: int, b: int) -> int:
          return a + b
      ''')
      print(f"Created module at {module_file}")

requirements:
  run:
    - python

tests:
  # Test 1: Python import test (embedded in package)
  - python:
      imports:
        - demo_module

  # Test 2: Package contents check (runs at build time)
  - package_contents:
      files:
        - site-packages/demo_module.py
about:
  license: MIT
"""

# Parse and render the recipe
demo_recipe = Stage0Recipe.from_yaml(test_recipe_yaml)
demo_variants = VariantConfig()
demo_render = RenderConfig()
demo_results = demo_recipe.render(demo_variants, demo_render)

print("Recipe with Tests Created")
print("=" * 60)
print(f"Package: {demo_recipe.package.name}")
print(f"Version: {demo_recipe.package.version}")

# Build the package (skip tests during build, we'll run them manually)
print("\nBuilding package...")
variant = demo_results[0]
from rattler_build import ToolConfiguration

tool_config = ToolConfiguration(test_strategy="skip")
build_result = variant.run_build(
    tool_config=tool_config,
)

built_package_path = build_result.packages[0]
print(f"Built: {built_package_path}")
Recipe with Tests Created
============================================================
Package: test-demo-package
Version: 1.0.0

Building package...
Built: /tmp/rattler_build_opdlypou/output/noarch/test-demo-package-1.0.0-pyh4616a5c_0.conda

Step 2: Loading a Package#

Use Package.from_file() to load a built package. This reads the package metadata without extracting the entire archive.

# Load the package
pkg = Package.from_file(built_package_path)

print("Package Loaded Successfully!")
print("=" * 60)
print(f"Path: {pkg.path}")
print(f"Type: {type(pkg).__name__}")
print(f"\nString representation: {repr(pkg)}")
Package Loaded Successfully!
============================================================
Path: /tmp/rattler_build_opdlypou/output/noarch/test-demo-package-1.0.0-pyh4616a5c_0.conda
Type: Package

String representation: Package(test-demo-package-1.0.0-pyh4616a5c_0)

Step 3: Inspecting Package Metadata#

The Package class provides direct access to all metadata from index.json:

print("Package Metadata")
print("=" * 60)
print(f"Name:           {pkg.name}")
print(f"Version:        {pkg.version}")
print(f"Build string:   {pkg.build_string}")
print(f"Build number:   {pkg.build_number}")
print(f"Subdir:         {pkg.subdir}")
print(f"NoArch:         {pkg.noarch}")
print(f"License:        {pkg.license}")
print(f"Arch:           {pkg.arch}")
print(f"Platform:       {pkg.platform}")
print(f"Timestamp:      {pkg.timestamp}")

print("\nArchive Information")
print("-" * 40)
print(f"Archive type:   {pkg.archive_type}")
print(f"Filename:       {pkg.filename}")

print("\nDependencies")
print("-" * 40)
print("Runtime dependencies (depends):")
for dep in pkg.depends:
    print(f"  - {dep}")

print("\nConstraints (constrains):")
if pkg.constrains:
    for constraint in pkg.constrains:
        print(f"  - {constraint}")
else:
    print("  (none)")
Package Metadata
============================================================
Name:           test-demo-package
Version:        1.0.0
Build string:   pyh4616a5c_0
Build number:   0
Subdir:         noarch
NoArch:         python
License:        MIT
Arch:           None
Platform:       None
Timestamp:      2026-03-03 13:25:51.035000+00:00

Archive Information
----------------------------------------
Archive type:   conda
Filename:       test-demo-package-1.0.0-pyh4616a5c_0.conda

Dependencies
----------------------------------------
Runtime dependencies (depends):
  - python

Constraints (constrains):
  (none)
# Convert to dictionary for programmatic access
metadata_dict = pkg.to_dict()

print("Metadata as Dictionary")
print("=" * 60)

print(json.dumps(metadata_dict, indent=2, default=str))
Metadata as Dictionary
============================================================
{
  "name": "test-demo-package",
  "version": "1.0.0",
  "build_string": "pyh4616a5c_0",
  "build_number": 0,
  "subdir": "noarch",
  "noarch": "python",
  "depends": [
    "python"
  ],
  "constrains": [],
  "license": "MIT",
  "license_family": null,
  "timestamp": "2026-03-03 13:25:51.035000+00:00",
  "arch": null,
  "platform": null,
  "path": "/tmp/rattler_build_opdlypou/output/noarch/test-demo-package-1.0.0-pyh4616a5c_0.conda",
  "archive_type": "conda",
  "filename": "test-demo-package-1.0.0-pyh4616a5c_0.conda"
}

Step 4: Listing Package Contents#

The files property lists all files contained in the package (from paths.json):

print("Package Contents")
print("=" * 60)

files = pkg.files
print(f"Total files: {len(files)}")
print("\nAll files:")

# Group files by directory
from collections import defaultdict

dirs = defaultdict(list)
for f in files:
    parts = f.split("/")
    if len(parts) > 1:
        dirs[parts[0]].append(f)
    else:
        dirs["(root)"].append(f)

for dir_name, dir_files in sorted(dirs.items()):
    print(f"\n  {dir_name}/")
    for f in sorted(dir_files):
        print(f"    {f}")
Package Contents
============================================================
Total files: 1

All files:

  site-packages/
    site-packages/demo_module.py

Step 5: Discovering Embedded Tests#

Packages built with rattler-build can include embedded tests in info/tests/tests.yaml. Let's inspect them:

print("Embedded Tests")
print("=" * 60)
print(f"Number of tests: {pkg.test_count}")

pkg_tests = pkg.tests
for test in pkg_tests:
    print(f"\nTest {test.index}: {type(test).__name__}")
    print("-" * 40)
    print(f"  Type: {type(test).__name__}")
    print(f"  Index: {test.index}")
    print(f"  Repr: {repr(test)}")
Embedded Tests
============================================================
Number of tests: 1

Test 0: PythonTest
----------------------------------------
  Type: PythonTest
  Index: 0
  Repr: PythonTest(imports=['demo_module'], pip_check=True)

Step 6: Inspecting Specific Test Types#

Each test type has specific properties. Use Python's pattern matching (3.10+) to handle different test types:

from rattler_build.package import PythonTest, CommandsTest, PackageContentsTest

print("Test Type Details")
print("=" * 60)

for test in pkg_tests:
    print(f"\nTest {test.index}: {type(test).__name__}")
    print("-" * 40)

    match test:
        case PythonTest() as py_test:
            print("  Python Test:")
            print(f"    Imports: {py_test.imports}")
            print(f"    Pip check: {py_test.pip_check}")
            if py_test.python_version:
                pv = py_test.python_version
                if pv.is_none():
                    print("    Python version: any")
                elif pv.as_single():
                    print(f"    Python version: {pv.as_single()}")
                elif pv.as_multiple():
                    print(f"    Python versions: {pv.as_multiple()}")

        case CommandsTest() as cmd_test:
            print("  Commands Test:")
            print(f"    Script: {cmd_test.script}")
            print(f"    Run requirements: {cmd_test.requirements_run}")
            print(f"    Build requirements: {cmd_test.requirements_build}")

        case PackageContentsTest() as pc_test:
            print("  Package Contents Test:")
            print(f"    Strict mode: {pc_test.strict}")

            sections = [
                ("files", pc_test.files),
                ("site_packages", pc_test.site_packages),
                ("bin", pc_test.bin),
                ("lib", pc_test.lib),
                ("include", pc_test.include),
            ]

            for name, checks in sections:
                if checks.exists or checks.not_exists:
                    print(f"    {name}:")
                    if checks.exists:
                        print(f"      exists: {checks.exists}")
                    if checks.not_exists:
                        print(f"      not_exists: {checks.not_exists}")

        case _:
            print(f"  Other test type: {type(test).__name__}")
Test Type Details
============================================================

Test 0: PythonTest
----------------------------------------
  Python Test:
    Imports: ['demo_module']
    Pip check: True
    Python version: any

Step 7: Running Tests#

Now let's run the tests! You can run individual tests by index or all tests at once:

print("Running Individual Tests")
print("=" * 60)

for i in range(pkg.test_count):
    print(f"\nRunning test {i}...")
    result = pkg.run_test(i)

    status = "PASS" if result.success else "FAIL"
    print(f"   {status}")
    print(f"   Test index: {result.test_index}")

    if result.output:
        print(f"   Output ({len(result.output)} lines):")
        for line in result.output[:5]:  # Show first 5 lines
            print(f"     {line}")
        if len(result.output) > 5:
            print(f"     ... and {len(result.output) - 5} more lines")
Running Individual Tests
============================================================

Running test 0...
   PASS
   Test index: 0
print("Running All Tests at Once")
print("=" * 60)

all_results = pkg.run_tests()

print(f"\nTotal tests: {len(all_results)}")
passed = sum(1 for r in all_results if r.success)
failed = len(all_results) - passed

print(f"Passed: {passed}")
print(f"Failed: {failed}")

print("\nResults summary:")
for result in all_results:
    status = "PASS" if result.success else "FAIL"
    print(f"  {status} Test {result.test_index}")

    # TestResult can be used as a boolean
    if result:
        print("     (result is truthy)")
    else:
        print("     (result is falsy)")
Running All Tests at Once
============================================================

Total tests: 1
Passed: 1
Failed: 0

Results summary:
  PASS Test 0
     (result is truthy)

Step 8: Using Test Results#

The TestResult object provides: - success: Boolean indicating pass/fail - test_index: Which test was run - output: List of output/log lines - Can be used directly as a boolean in conditions

print("TestResult Properties")
print("=" * 60)

for result in all_results:
    print(f"\nTest {result.test_index}:")
    print(f"  success:    {result.success}")
    print(f"  test_index: {result.test_index}")
    print(f"  output:     {len(result.output)} lines")
    print(f"  bool():     {bool(result)}")
    print(f"  repr():     {repr(result)}")

# Example: Filter results
print("\n" + "=" * 60)
passed_tests = [r for r in all_results if r]
failed_tests = [r for r in all_results if not r]

print(f"Passed tests: {[r.test_index for r in passed_tests]}")
print(f"Failed tests: {[r.test_index for r in failed_tests]}")
TestResult Properties
============================================================

Test 0:
  success:    True
  test_index: 0
  output:     0 lines
  bool():     True
  repr():     TestResult(index=0, status=PASS)

============================================================
Passed tests: [0]
Failed tests: []

Step 9: Running Tests with Custom Configuration#

You can customize test execution with channels, authentication, and other options:

print("Test Configuration Options")
print("=" * 60)

print(
    """
Available options for run_test() and run_tests():

- channel: List[str]           # Channels to use for dependencies
                               # e.g., ["conda-forge", "defaults"]

- channel_priority: str        # "disabled", "strict", or "flexible"

- debug: bool                  # Keep test environment for debugging
                               # Default: False

- auth_file: str | Path        # Path to authentication file

- allow_insecure_host: List[str]  # Hosts to allow insecure connections

- compression_threads: int     # Number of compression threads

- use_bz2: bool               # Enable bz2 repodata (default: True)
- use_zstd: bool              # Enable zstd repodata (default: True)
- use_sharded: bool           # Enable sharded repodata (default: True)
"""
)

# Example with custom channel
print("\nExample: Running test with conda-forge channel:")
result = pkg.run_test(
    0,
    channel=["conda-forge"],
    channel_priority="strict",
)
print(f"  Result: {'PASS' if result.success else 'FAIL'}")
Test Configuration Options
============================================================

Available options for run_test() and run_tests():

- channel: List[str]           # Channels to use for dependencies
                               # e.g., ["conda-forge", "defaults"]

- channel_priority: str        # "disabled", "strict", or "flexible"

- debug: bool                  # Keep test environment for debugging
                               # Default: False

- auth_file: str | Path        # Path to authentication file

- allow_insecure_host: List[str]  # Hosts to allow insecure connections

- compression_threads: int     # Number of compression threads

- use_bz2: bool               # Enable bz2 repodata (default: True)
- use_zstd: bool              # Enable zstd repodata (default: True)
- use_sharded: bool           # Enable sharded repodata (default: True)


Example: Running test with conda-forge channel:
  Result: PASS

Step 10: Rebuilding Packages#

Conda packages built with rattler-build embed their recipe, allowing you to rebuild them from scratch. This is useful for verifying reproducibility - checking if a package can be rebuilt to produce identical output.

The rebuild() method extracts the embedded recipe and rebuilds the package, then compares SHA256 hashes to verify if the builds are identical.

print("Rebuilding Package")
print("=" * 60)

# Rebuild the package and compare hashes
rebuild_result = pkg.rebuild(test="skip")

print(f"Original package: {rebuild_result.original_path}")
print(f"Rebuilt package:  {rebuild_result.rebuilt_path}")
print()
print(f"Original SHA256:  {rebuild_result.original_sha256}")
print(f"Rebuilt SHA256:   {rebuild_result.rebuilt_sha256}")
print()
print(f"Identical (reproducible): {rebuild_result.is_identical}")
Rebuilding Package
============================================================
Original package: /tmp/rattler_build_opdlypou/output/noarch/test-demo-package-1.0.0-pyh4616a5c_0.conda
Rebuilt package:  output/test-demo-package-1.0.0-pyh4616a5c_0-rebuilt-20260303-132558.conda

Original SHA256:  496816c4c66b796e39440968b21d110a0dabab52d69b93fc29620c2741d63fa5
Rebuilt SHA256:   496816c4c66b796e39440968b21d110a0dabab52d69b93fc29620c2741d63fa5

Identical (reproducible): True

The RebuildResult provides access to the rebuilt package for further inspection:

# Access the rebuilt package for inspection
rebuilt_pkg = rebuild_result.rebuilt_package

print("Rebuilt Package Details")
print("=" * 60)
print(f"Name:         {rebuilt_pkg.name}")
print(f"Version:      {rebuilt_pkg.version}")
print(f"Build string: {rebuilt_pkg.build_string}")
print(f"Path:         {rebuilt_pkg.path}")
print(f"Files:        {len(rebuilt_pkg.files)} files")
Rebuilt Package Details
============================================================
Name:         test-demo-package
Version:      1.0.0
Build string: pyh4616a5c_0
Path:         output/test-demo-package-1.0.0-pyh4616a5c_0-rebuilt-20260303-132558.conda
Files:        1 files

Summary#

In this tutorial, you learned how to:

  • Load packages: Use Package.from_file() to load .conda or .tar.bz2 files
  • Inspect metadata: Access name, version, depends, license, etc.
  • Archive information: Use archive_type and filename to get package format details
  • List contents: Use files to see all files in the package
  • Discover tests: Access tests to see embedded test definitions
  • Inspect test types: Use pattern matching with PythonTest, CommandsTest, PackageContentsTest, etc.
  • Run tests: Use run_test(index) or run_tests() to execute tests
  • Handle results: TestResult provides success, output, and works as boolean
  • Rebuild packages: Use rebuild() to verify reproducibility by comparing SHA256 hashes

The Package API provides a complete interface for inspecting, testing, and rebuilding conda packages programmatically!