Inspecting Built Packages#
This tutorial teaches you how to work with built conda packages:
- Load packages from
.condaor.tar.bz2files - Inspect package metadata (name, version, dependencies)
- List all files contained in the package
- Discover and inspect embedded tests
- Run tests programmatically and capture results
- 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.condaor.tar.bz2files - Inspect metadata: Access
name,version,depends,license, etc. - Archive information: Use
archive_typeandfilenameto get package format details - List contents: Use
filesto see all files in the package - Discover tests: Access
teststo see embedded test definitions - Inspect test types: Use pattern matching with
PythonTest,CommandsTest,PackageContentsTest, etc. - Run tests: Use
run_test(index)orrun_tests()to execute tests - Handle results:
TestResultprovidessuccess,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!