Metadata-Version: 2.4
Name: arcsmith
Version: 0.0.2
Summary: Utilities for building clean, maintainable ArcPy toolboxes
License-Expression: MIT
License-File: LICENSE
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Requires-Python: >=3.13
Description-Content-Type: text/markdown

# arcsmith

> Utilities for building clean, maintainable ArcPy toolboxes.

arcsmith reduces the boilerplate common to ArcPy toolbox development — parameter validation, symbology application, and field management — so that toolbox code remains readable and consistent.

[![Python](https://img.shields.io/badge/python-3.x-blue.svg)](https://www.python.org/)
[![PyPI](https://img.shields.io/pypi/v/arcsmith.svg)](https://pypi.org/project/arcsmith/)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)

---

## Installation

```bash
pip install arcsmith
```

> **Note:** `arcsmith` depends on `arcpy`, which ships with ArcGIS Pro and is not available via pip. Ensure you are running in an environment where `arcpy` is accessible.

---

## Usage

```python
import arcsmith

# Layer symbology
arcsmith.layer.add_w_sym("path/to/fc", "path/to/sym.lyrx", target_map)

# Parameter management (Inside of toolbox's updateParameters)
if arcsmith.param.state(parameters[0]) == 'pending':
        parameters[1].value = None
```

---

## Modules

- [`arcsmith.param`](#arcsmithparam): Parameter state management, dropdowns, and checkbox-driven visibility
- [`arcsmith.layer`](#arcsmithlayer): Adding layers and applying symbology
- [`arcsmith.fields`](#arcsmithfields): Field introspection, management, and mapping *(coming soon)*

---

## arcsmith.param

Helpers for managing `arcpy.Parameter` state inside `ToolValidator`.

A key concept throughout this module is **parameter state**. Understanding whether a parameter has been changed by the user and whether it has passed through ArcPy's validation cycle is the foundation of `updateParameters` logic that ArcSmith simplifies.

---

### `state`

Returns a string describing the combined `altered` / `hasBeenValidated` state of a parameter.

```python
state(param: arcpy.Parameter) -> str
```

**States**

| State       | `.altered` | `.hasBeenValidated` | Description                              |
|-------------|------------|---------------------|------------------------------------------|
| `fresh`     | `False`    | `False`             | Initial, untouched state                 |
| `pending`   | `True`     | `False`             | Changed, awaiting validation             |
| `settled`   | `False`    | `True`              | Validated, not changed since             |
| `confirmed` | `True`     | `True`              | Changed and validated                    |

The `'pending'` state is the most important to detect. It marks the moment a value was **just** changed, and is the right time to cascade resets or re-populate dependent controls.

**Example**

```python
state = state(parameters[0])

if state == 'pending':
    # parameter was just changed
    ...
```

---

### `cascade_clear`

Resets downstream parameters to `None` when an upstream parameter is in a `pending` state.

```python
cascade_clear(trigger_param, downstream_params)
```

**Parameters**

| Parameter           | Type                    | Default  | Description                                               |
|---------------------|-------------------------|----------|-----------------------------------------------------------|
| `trigger_param`     | `arcpy.Parameter`       | required | The parameter whose change triggers the cascade.          |
| `downstream_params` | `list[arcpy.Parameter]` | required | Parameters to clear when `trigger_param` is `'pending'`.  |

Safe to call unconditionally on every `updateParameters` pass. If `trigger_param` is not `'pending'` the call is a no-op.

**Example**

```python
# Clear workspace and layer fields whenever the geodatabase selection changes
cascade_clear(params[0], [params[1], params[2]])
```

---

### `drop_populate`

Sets or replaces a parameter's dropdown filter list.

```python
drop_populate(param, values, overwrite_empty=False)
```

**Parameters**

| Parameter         | Type              | Default  | Description                                                                   |
|-------------------|-------------------|----------|-------------------------------------------------------------------------------|
| `param`           | `arcpy.Parameter` | required | The dropdown parameter to populate.                                           |
| `values`          | `list[str]`       | required | Options to display in the dropdown.                                           |
| `overwrite_empty` | `bool`            | `False`  | If `True`, updates the filter even when `values` is empty, clearing the list. |

By default, an empty `values` list leaves the existing filter untouched. This prevents accidentally clearing a valid dropdown when an upstream dependency hasn't been set yet.

**Example**

```python
# Populate field names derived from a selected feature class
fields = [f.name for f in arcpy.ListFields(params[0].value)]
drop_populate(params[1], fields)

# Explicitly clear a dropdown
drop_populate(params[1], [], overwrite_empty=True)
```

---

### `checkbox_dependence`

Enables or disables dependent parameters based on a controlling checkbox, assigning placeholder values when hidden to keep required parameters satisfied.

```python
checkbox_dependence(
    controlling_checkbox,
    dependents,
    placeholder=None,
    always_show_dependents=False
)
```

**Behaviour**

| Checkbox state | Effect                                                        |
|----------------|---------------------------------------------------------------|
| Unchecked      | Dependents disabled; placeholder value(s) assigned            |
| Just checked   | Dependents enabled; values cleared to `None`                  |
| Otherwise      | No-op. Existing user input is preserved.                      |

**Raises:** `ValueError` if `placeholder` is a list whose length differs from `dependents`.

**Examples**

```python
# Broadcast a single placeholder to all dependents
checkbox_dependence(cb, [p1, p2, p3], placeholder="N/A")

# Single dependent - no list required
checkbox_dependence(cb, p1, placeholder=0)

# Mixed types, one placeholder per dependent
checkbox_dependence(cb, [p1, p2, p3], placeholder=["N/A", 0, None])

# Keep dependents visible but disabled when unchecked
checkbox_dependence(cb, [p1, p2], placeholder="N/A", always_show_dependents=True)
```

---

### `dynamic_dropdown`

Enables the correct group of dependent parameters based on the selected dropdown option, disabling all others.

```python
dynamic_dropdown(controlling_dropdown, option_map, placeholder_map=None)
```

**Parameters**

| Parameter              | Type                               | Default  | Description                                                                                                                              |
|------------------------|------------------------------------|----------|------------------------------------------------------------------------------------------------------------------------------------------|
| `controlling_dropdown` | `arcpy.Parameter`                  | required | Dropdown that controls which parameter group is active.                                                                                  |
| `option_map`           | `dict[str, list[arcpy.Parameter]]` | required | Maps each option label to the parameters that should be enabled when that option is selected.                                            |
| `placeholder_map`      | `dict[str, list]`                  | `None`   | Maps each option label to placeholder values assigned when the dropdown is `pending`. `None` entries skip assignment for that parameter. |

**Behaviour**

| Dropdown state               | Effect                                                         |
|------------------------------|----------------------------------------------------------------|
| Any state                    | All managed parameters disabled first                          |
| Valid selection, not pending | Active group re-enabled; existing values preserved             |
| Valid selection, pending     | Active group cleared to placeholders (or `None`) and enabled   |

**Raises:** `ValueError` if any `placeholder_map` list length differs from its `option_map` counterpart.

**Examples**

```python
# Show different parameters depending on the input type selected
dynamic_dropdown(
    params[0],
    option_map={
        "Shapefile":     [params[1]],
        "Feature Class": [params[2], params[3]],
    }
)

# With placeholder values applied on change
dynamic_dropdown(
    params[0],
    option_map={
        "Shapefile":     [params[1]],
        "Feature Class": [params[2], params[3]],
    },
    placeholder_map={
        "Shapefile":     [None],
        "Feature Class": ["N/A", 0],
    }
)
```

---

## arcsmith.layer

Helpers for adding layers to a map and applying `.lyrx` symbology programmatically.

---

### `add_w_sym`

Adds a data source to a map as a new layer and immediately applies symbology from a `.lyrx` file.

```python
add_w_sym(lyr_src, lyrx_src, target_map, lyr_name=None)
```

**Parameters**

| Parameter    | Type            | Default  | Description                                                         |
|--------------|-----------------|----------|---------------------------------------------------------------------|
| `lyr_src`    | `str` or `Path` | required | Path to the data source (feature class, raster, etc.) to add.      |
| `lyrx_src`   | `str` or `Path` | required | Path to the `.lyrx` file containing the symbology to apply.        |
| `target_map` | `arcpy.mp.Map`  | required | Map object to add the layer to.                                     |
| `lyr_name`   | `str`           | `None`   | Display name for the layer in the TOC. Defaults to the source name. |

**Returns:** `arcpy.mp.Layer`. The newly added layer with symbology applied.

**Example**

```python
# Add a feature class and apply symbology
lyr = add_w_sym("path/to/fc", "path/to/sym.lyrx", target_map)

# Add with a custom display name
lyr = add_w_sym("path/to/fc", "path/to/sym.lyrx", target_map, lyr_name="Study Area")
```

---

### `apply_lyrx`

Applies symbology from a `.lyrx` file to one or more layers already present in the map TOC, matched by display name or data source path.

```python
apply_lyrx(lyrx_src, target_map, lyr_name=None, lyr_source=None, geom_type=None)
```

**Parameters**

| Parameter    | Type                                                        | Default  | Description                                                                                                 |
|--------------|-------------------------------------------------------------|----------|-------------------------------------------------------------------------------------------------------------|
| `lyrx_src`   | `str` or `Path`                                             | required | Path to the `.lyrx` file containing the symbology to apply.                                                |
| `target_map` | `arcpy.mp.Map`                                              | required | Map object containing the layer(s) to update.                                                               |
| `lyr_name`   | `str`                                                       | `None`   | Display name to match; **all** layers with this name are updated.                                           |
| `lyr_source` | `str` or `Path`                                             | `None`   | Data source path to match; only the **first** exact match is updated.                                       |
| `geom_type`  | `'Point'` \| `'Polyline'` \| `'Polygon'` \| `'Multipoint'` | `None`   | Geometry type filter when matching by `lyr_name`. Case-insensitive. Ignored when matching by `lyr_source`. |

**Returns:** `list[arcpy.mp.Layer]`. All layers that were updated.

**Raises:**
- `ValueError` if neither or both of `lyr_name` and `lyr_source` are provided.
- `ValueError` if no matching layers are found in the map.

**Example**

```python
# Update all layers named "rivers"
lyrs = apply_lyrx("path/to/sym.lyrx", target_map, lyr_name="rivers")

# Update only polyline "rivers" layers
lyrs = apply_lyrx("path/to/sym.lyrx", target_map, lyr_name="rivers", geom_type="Polyline")

# Update a single layer by data source path
lyrs = apply_lyrx("path/to/sym.lyrx", target_map, lyr_source="path/to/fc")
```

---

## arcsmith.fields

> **Coming soon.** This module is planned for a future release.

Utilities for field introspection, management, and mapping. These functions return copies of the input rather than modifying it in place, and accept both layer objects and path strings.

**Field introspection**

- `list_fields`: Return field names for a layer or table, with optional filters for field type and to exclude system fields like `OID` and `Shape`.
- `unique_values`: Return sorted unique values for a given field.
- `field_exists`: Check whether a named field exists, useful for defensive scripting before adds or deletes.

**Field management**

- `keep_fields`: Return a copy with all fields removed except a specified list.
- `drop_fields`: Return a copy with a specified list of fields removed. Skips missing names rather than erroring.
- `rename_field`: Return a copy with a field renamed. Handled via add/calculate/delete internally since ArcPy has no native rename.

**Field mapping**

- `build_field_map`: Construct a `FieldMappings` object from a layer, optionally filtered to a subset of fields, as a clean starting point for tools like `Merge` or `Append`.
- `field_map_rename`: Apply a rename dictionary to a `FieldMappings` object, avoiding verbose `FieldMap` boilerplate.
- `align_fields`: Given a source and target, build a field mapping that matches fields by name, useful for `Append` with schema mismatches.

---

## License

[MIT](LICENSE)