Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,15 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6

- name: Set up Python
uses: actions/setup-python@v5
uses: actions/setup-python@v6
with:
python-version: "3.13"

- name: Install uv
uses: astral-sh/setup-uv@v3
uses: astral-sh/setup-uv@v8.1.0
with:
enable-cache: true
cache-dependency-glob: "**/pyproject.toml"
Expand All @@ -43,12 +43,12 @@ jobs:
touch _build/html/.nojekyll

- name: Setup Pages
uses: actions/configure-pages@v4
uses: actions/configure-pages@v6
with:
enablement: true

- name: Upload artifact
uses: actions/upload-pages-artifact@v3
uses: actions/upload-pages-artifact@v5
with:
path: ./docs/_build/html

Expand All @@ -61,4 +61,4 @@ jobs:
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
uses: actions/deploy-pages@v5
26 changes: 13 additions & 13 deletions .github/workflows/pr.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,15 @@ jobs:
name: Lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6

- name: Set up Python
uses: actions/setup-python@v5
uses: actions/setup-python@v6
with:
python-version: "3.13"

- name: Install uv
uses: astral-sh/setup-uv@v3
uses: astral-sh/setup-uv@v8.1.0
with:
enable-cache: true
cache-dependency-glob: "**/pyproject.toml"
Expand All @@ -38,15 +38,15 @@ jobs:
python-version: ["3.10", "3.11", "3.12", "3.13"]

steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python-version }}

- name: Install uv
uses: astral-sh/setup-uv@v3
uses: astral-sh/setup-uv@v8.1.0
with:
enable-cache: true
cache-dependency-glob: "**/pyproject.toml"
Expand All @@ -63,15 +63,15 @@ jobs:
name: Build Documentation
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6

- name: Set up Python
uses: actions/setup-python@v5
uses: actions/setup-python@v6
with:
python-version: "3.13"

- name: Install uv
uses: astral-sh/setup-uv@v3
uses: astral-sh/setup-uv@v8.1.0
with:
enable-cache: true
cache-dependency-glob: "**/pyproject.toml"
Expand All @@ -88,15 +88,15 @@ jobs:
name: YAML Policy Tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6

- name: Set up Python
uses: actions/setup-python@v5
uses: actions/setup-python@v6
with:
python-version: "3.13"

- name: Install uv
uses: astral-sh/setup-uv@v3
uses: astral-sh/setup-uv@v8.1.0
with:
enable-cache: true
cache-dependency-glob: "**/pyproject.toml"
Expand All @@ -113,7 +113,7 @@ jobs:
name: Check changelog fragment
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Check for changelog fragment
run: |
FRAGMENTS=$(find changelog.d -type f ! -name '.gitkeep' | wc -l)
Expand Down
22 changes: 12 additions & 10 deletions .github/workflows/push.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,15 @@ jobs:
name: Lint, Test and Build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6

- name: Set up Python
uses: actions/setup-python@v5
uses: actions/setup-python@v6
with:
python-version: "3.13"

- name: Install uv
uses: astral-sh/setup-uv@v3
uses: astral-sh/setup-uv@v8.1.0
with:
enable-cache: true
cache-dependency-glob: "**/pyproject.toml"
Expand Down Expand Up @@ -50,13 +50,13 @@ jobs:
needs: lint-test-build

steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}

- name: Set up Python
uses: actions/setup-python@v5
uses: actions/setup-python@v6
with:
python-version: "3.13"

Expand All @@ -81,15 +81,15 @@ jobs:
needs: lint-test-build

steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6

- name: Set up Python
uses: actions/setup-python@v5
uses: actions/setup-python@v6
with:
python-version: "3.13"

- name: Install uv
uses: astral-sh/setup-uv@v3
uses: astral-sh/setup-uv@v8.1.0
with:
enable-cache: true
cache-dependency-glob: "**/pyproject.toml"
Expand Down Expand Up @@ -117,10 +117,12 @@ jobs:
# if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags')
#
# steps:
# - uses: actions/checkout@v4
# - uses: actions/checkout@v6
#
# - name: Set up Python
# uses: actions/setup-python@v5
# uses: actions/setup-python@v6
with:
python-version: "3.12"
# with:
# python-version: "3.13"
#
Expand Down
84 changes: 62 additions & 22 deletions policyengine_au/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,31 @@

import pytest
import yaml
from pathlib import Path
import re
import numpy as np
from policyengine_au import AustralianTaxBenefitSystem
from policyengine_core.simulations import Simulation


PERIOD_PATTERN = re.compile(r"^\d{4}(-\d{2}(-\d{2})?)?$")


def is_period_key(key):
return str(key) == "ETERNITY" or bool(PERIOD_PATTERN.match(str(key)))


def normalize_input_values(value, period):
if isinstance(value, dict):
if value and all(is_period_key(key) for key in value):
return value
return {
key: normalize_input_values(child, period) for key, child in value.items()
}
if isinstance(value, list):
return value
return {period: value}


def pytest_collect_file(parent, path):
"""Custom collector for YAML test files."""
if path.ext == ".yaml" and path.basename.startswith("test_"):
Expand Down Expand Up @@ -44,34 +64,54 @@ def runtest(self):
# Build situation from input
situation = self.spec.get("input", {})
period = str(self.spec.get("period", "2024"))
situation = normalize_input_values(situation, period)

# Create simulation
simulation = Simulation(tax_benefit_system=system, situation=situation)

# Check outputs
expected_outputs = self.spec.get("output", {})
for variable_name, expected_value in expected_outputs.items():
if isinstance(expected_value, dict):
# Handle per-entity outputs
for entity_name, entity_expected in expected_value.items():
calculated = simulation.calculate(variable_name, period)
# Would need entity index mapping here
pass
else:
# Simple case: single value
calculated = simulation.calculate(variable_name, period)
if hasattr(calculated, "__len__"):
calculated = calculated[0]

# Allow small tolerance for floating point comparisons
if isinstance(expected_value, (int, float)):
assert abs(calculated - expected_value) < 0.01, (
f"{variable_name}: expected {expected_value}, got {calculated}"
)
else:
assert calculated == expected_value, (
f"{variable_name}: expected {expected_value}, got {calculated}"
person_ids = list(situation.get("people", {}).keys())

for output_key, expected_value in expected_outputs.items():
if output_key in system.variables:
self.assert_variable(simulation, output_key, period, expected_value)
elif isinstance(expected_value, dict) and output_key in person_ids:
for variable_name, entity_expected in expected_value.items():
person_index = person_ids.index(output_key)
self.assert_variable(
simulation,
variable_name,
period,
entity_expected,
index=person_index,
)
else:
raise AssertionError(f"Unknown output target: {output_key}")

def assert_variable(
self, simulation, variable_name, period, expected_value, index=0
):
calculated = simulation.calculate(variable_name, period)
if isinstance(calculated, np.ndarray):
calculated = calculated[index]
elif hasattr(calculated, "__len__") and not isinstance(
calculated, (str, bytes)
):
calculated = calculated[index]

if isinstance(expected_value, bool):
assert bool(calculated) is expected_value, (
f"{variable_name}: expected {expected_value}, got {calculated}"
)
elif isinstance(expected_value, (int, float)):
assert abs(calculated - expected_value) < 0.01, (
f"{variable_name}: expected {expected_value}, got {calculated}"
)
else:
assert calculated == expected_value, (
f"{variable_name}: expected {expected_value}, got {calculated}"
)

def reportinfo(self):
"""Report test location."""
Expand Down
8 changes: 4 additions & 4 deletions policyengine_au/tests/policy/baseline/test_income_tax.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@
employment_income: 120_000
output:
taxable_income: 120_000
income_tax: 28_467 # Progressive tax calculation
income_tax: 29_467 # Progressive tax calculation
medicare_levy: 2_400 # 2% of $120,000

- name: High income earner with $200,000 income
Expand All @@ -69,13 +69,13 @@
medicare_levy: 4_000 # 2% of $200,000

- name: Stage 3 tax cuts example - $150,000 income
period: 2024-07-01
period: 2025
input:
people:
person_1:
age: 40
employment_income: 150_000
output:
taxable_income: 150_000
income_tax: 39_567 # With Stage 3 tax cuts
medicare_levy: 3_000 # 2% of $150,000
income_tax: 37_642 # With Stage 3 tax cuts
medicare_levy: 3_000 # 2% of $150,000
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ def formula(person, period, parameters):
p_eligibility = parameters(period).gov.dss.age_pension.eligibility

# Current age threshold is 67
age_threshold = p_eligibility.age_threshold.current
age_threshold = p_eligibility.age_threshold

# Check age eligibility
age_eligible = age >= age_threshold
Expand Down
4 changes: 3 additions & 1 deletion policyengine_au/variables/gov/states/payroll_tax.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from policyengine_core.model_api import *
from policyengine_au.entities import *
from policyengine_au.variables.input.demographics.state import StateCode
import numpy as np


class state_payroll_tax(Variable):
Expand Down Expand Up @@ -30,7 +32,7 @@ def formula(household, period, parameters):
}

# Calculate tax for each state and select the appropriate one
tax = zeros(household.count)
tax = np.zeros(household.count)
for state_code, tax_variable in state_tax_map.items():
is_state = state == state_code
state_tax = household(tax_variable, period)
Expand Down