This guide covers development workflow, testing, code style, and how to contribute to the Nomic codebase.
- Python 3.8 or higher
uv(recommended) orpipfor package management
-
Clone the repository (if applicable)
-
Install dependencies:
uv sync --dev # or pip install -e ".[dev]"
This installs:
- Runtime dependencies:
pandas,python-dotenv - Development dependencies:
pytest,ruff,pyright
- Runtime dependencies:
-
Verify installation:
nomic rules num pytest
The project has two types of tests:
- Unit Tests (
tests/test_*.py) - Test individual functions and modules in isolation - Integration Tests (
tests/integration/*.sh) - Test complete workflows using the CLI
Run all unit tests:
pytestRun specific unit test file:
pytest tests/test_players.pyRun with verbose output:
pytest -vRun integration tests:
# Run all integration tests
./tests/integration/test_game_workflows.sh
# Or run individually (they're bash scripts)
./tests/integration/test_game_workflows.shUnit Tests (tests/) are organized to mirror the source structure:
test_players.py- Tests forsrc/players.pytest_rules.py- Tests forsrc/rules.pytest_proposals.py- Tests forsrc/proposals.pytest_metadata.py- Tests forsrc/metadata.py- etc.
Integration Tests (tests/integration/) test complete workflows:
test_game_workflows.sh- Complete game workflows organized by type:- Proposal workflows (enactment, amendment, repeal, transmutation)
- Voting workflows (unanimous voting, points, prohibited states)
- Turn workflows (turn order, complete turns, forfeiture)
- Rule state workflows (mutability, rule limits)
test_helpers.sh- Shared test helper functions
Integration tests focus on state change workflows as defined in the game rules, rather than code coverage. Each test is annotated with the relevant rule numbers being tested.
The state fixture provides a clean State object for each test:
def test_add_player(state: State):
from src.players import add
add(state, "Test Player")
assert len(state.player_metadata) == 1Use the @tests decorator to inject required rules into test state:
from src.implements import tests
@tests({105, 109, 203})
def test_vote_passed(state: State):
from src.actions import vote_passed
result = vote_passed(state, votes_for=[0, 1, 2], votes_against=[])
assert result is TrueThis ensures the state has the required rules before the test runs.
The project uses ruff for linting and formatting. Check code style:
ruff check src/Auto-fix issues:
ruff check --fix src/The project uses pyright for type checking:
pyright src/-
Line Length: 120 characters (configured in
pyproject.toml) -
Docstrings: Use triple-quoted strings for module and function documentation:
def add_player(state: State, name: str) -> None: """Add a new player to the game. Args: state: State object name: Full name of the player """
-
Type Hints: Use type hints for all function parameters and return values:
def get_player_id(state: State, name: str) -> int: ...
-
Imports: Organize imports (ruff handles this automatically):
- Standard library
- Third-party packages
- Local modules
Each module has a focused responsibility:
- Domain Logic:
players.py,rules.py,proposals.py - Actions:
actions.py(proposal resolution, turn processing) - Infrastructure:
state.py,metadata.py,paths.py,text_lists.py - Utilities:
type_utils.py,id_utils.py,implements.py - Interface:
cli.py
- Identify the module: Determine which module your feature belongs to
- Write the function: Follow existing patterns and style
- Add validation: Use
InvalidStateErrororInvalidProposalErrorfor validation failures - Add tests: Write comprehensive tests
- Add CLI command (if needed): Add command to
src/cli.py - Update documentation: Update relevant docs files
Let's say we want to add a function to get all active players:
-
Add to
src/players.py:def get_active_players(state: State) -> pd.DataFrame: """Get all active players. Args: state: State object Returns: DataFrame with active players """ df = state.player_metadata require_unique_index(df, "id") check_required_columns(df, {"active"}) return df[df["active"]].copy()
-
Add test to
tests/test_players.py:def test_get_active_players(state: State): from src.players import add, get_active_players add(state, "Player 1") add(state, "Player 2") active = get_active_players(state) assert len(active) == 2
-
Add CLI command (if needed):
def cmd_active_players(paths: Paths) -> None: """List all active players.""" state = State.load(paths) active = get_active_players(state) NOMIC_LOGGER.info("Active players:\n%s", active.to_string())
When implementing game rules, use the @implements decorator to track which rules are implemented:
from src.implements import implements
@implements({201, 202})
def process_turn(state: State) -> None:
"""Process a complete turn.
Implements:
- Rule 201: Players alternate in alphabetical order
- Rule 202: Turn consists of proposal + dice roll
"""
# Implementation
...- Document which rules: Always document which rules are implemented in the function docstring
- Use the decorator: Always use
@implementsfor functions that implement specific rules - Test with rules: Use
@testsdecorator in tests to ensure required rules exist - Keep implementations focused: One function should implement a coherent set of related rules
If you see InvalidStateError, check:
- That all required files exist
- That IDs/numbers are unique
- That referential integrity is maintained (e.g., proposers exist)
- That data types match expected types
If you see ValueError: Required rules not all implemented, check:
- That the rules exist in
game/rules.md - That the rules are in
game/rule_metadata.csv - That rule numbers match between files
If files aren't found:
- Check that
game/directory exists - Check
.envfile for custom paths - Verify default paths in
src/paths.py
-
Use logging: The
NOMIC_LOGGERprovides structured logging:from src.nomic_logger import NOMIC_LOGGER NOMIC_LOGGER.info("Debug: %s", value)
-
Inspect state: Load state and inspect DataFrames:
from src.state import State from src.paths import load_paths state = State.load(load_paths()) print(state.player_metadata) print(state.rules)
-
Run tests: Tests often reveal issues:
pytest -v tests/test_your_feature.py
-
Check CLI output: Run CLI commands to see error messages:
nomic players add "Test Player"
- Create a branch (if using git)
- Make changes: Follow code style and add tests
- Run tests: Ensure all tests pass
- Check style: Run
ruff checkandpyright - Update documentation: Update relevant docs if needed
- Submit changes: Create a pull request or submit patch
- All tests pass
- Code follows style guidelines (
ruff checkpasses) - Type checking passes (
pyrightpasses) - Documentation updated (if needed)
- New features have tests
-
@implementsdecorator used for rule implementations
When reviewing code, check:
- Correctness: Does it do what it's supposed to?
- Tests: Are there adequate tests?
- Style: Does it follow project style?
- Documentation: Is it well-documented?
- Edge cases: Are edge cases handled?
nomic/
├── game/ # Game state files (CSV + Markdown)
├── src/ # Source code
│ ├── cli.py # CLI interface
│ ├── players.py # Player management
│ ├── rules.py # Rule management
│ ├── proposals.py # Proposal management
│ ├── actions.py # Game actions
│ └── ...
├── tests/ # Test files
│ ├── test_*.py # Unit tests (pytest)
│ └── integration/ # Integration tests
│ ├── test_game_workflows.sh # Complete workflow tests
│ └── test_helpers.sh # Shared test helpers
├── docs/ # Documentation
├── pyproject.toml # Project configuration
└── README.md # Project overview
- Game State Guide - How to update game state
- Architecture Guide - System design overview
- API Reference - Detailed function documentation