Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
1adb651
First bite on typipng
Suor Feb 26, 2026
e1e8e57
Less XFAIL
Suor Feb 26, 2026
b51a6b6
Less Any more reveal_type()
Suor Feb 26, 2026
0a950e8
Drop mypy stubtest ini
Suor Feb 26, 2026
2f691d7
More type tests, fix R: comment to match properly, cover funcmakers, etc
Suor Feb 26, 2026
d8ac47a
Show unexpected error message and some fixes
Suor Feb 28, 2026
587f7b0
Add pyih machinery
Suor Feb 28, 2026
ed316e4
Fix FIX comments: docs, tests, translate_pyih refactoring
Suor Feb 28, 2026
ea5e517
Use Hashable instead of Any in is_distinct type stubs
Suor Mar 1, 2026
3aa9a3e
Add XPred and collection type parametrization to select stubs
Suor Mar 1, 2026
a5121bd
Add MutableMapping/Mapping overloads to walk type stubs
Suor Mar 1, 2026
0142d29
Add collection-of-pairs type tests for walk
Suor Mar 1, 2026
d3fa361
Add MutableMapping and collection-of-pairs support to walk_keys/walk_…
Suor Mar 1, 2026
27269ad
Add collection-of-pairs and abstract mapping support to compact stubs
Suor Mar 1, 2026
78d4914
Add MutableMapping and collection-of-pairs support to flip/project/omit
Suor Mar 1, 2026
0b0fcc7
Add MutableMapping overloads to set_in, update_in, del_in stubs
Suor Mar 1, 2026
cf72efa
Add collection-of-pairs support to select_keys and select_values
Suor Mar 1, 2026
a49ca7b
Fix lints
Suor Mar 1, 2026
85149fa
Add more FIX plans
Suor Mar 1, 2026
61da06b
Add COLS, MAPS and MUT_MAPS to .pyih to be DRY and not lazy
Suor Mar 1, 2026
b8f1d09
Add CLAUDE.md
Suor Mar 9, 2026
d6b77de
Make type_tests/run.py run all checkers when called without arguments
Suor Mar 9, 2026
c9b0d5b
Skip test_translate_pyih.py on Python < 3.12
Suor Mar 9, 2026
82a2e89
Rename _Func to _XFunc and include Callable[..., Any] in the union
Suor Mar 9, 2026
c205dd1
Add MAPS catch-all overload for select with _XFunc predicates
Suor Mar 9, 2026
872baae
Type join_with/merge_with to link dict value types to Callable param
Suor Mar 9, 2026
a509a39
Type compact to strip None from element type of invariant collections
Suor Mar 9, 2026
5bfed79
GEMINI.md -> AGENTS.md
Suor Apr 19, 2026
73f876e
Add translate_pyih.py refactor plans
Suor Apr 19, 2026
c57c20a
Fix type tests
Suor Apr 19, 2026
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
15 changes: 15 additions & 0 deletions .claude/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"hooks": {
"PreToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "python3 -c \"\nimport json, sys\nfrom pathlib import Path\ninp = json.load(sys.stdin)\npath = inp.get('file_path', '')\nif path.endswith('.pyi'):\n pyih = Path(path).with_suffix('.pyih')\n if pyih.exists():\n print(f'BLOCKED: {Path(path).name} is auto-generated from {pyih.name}. Edit the .pyih source instead, then run: python translate_pyih.py', file=sys.stderr)\n sys.exit(2)\n\""
}
]
}
]
}
}
1 change: 1 addition & 0 deletions AGENTS.md
81 changes: 81 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
## Project Overview

Funcy is a Python library of functional programming tools. It has zero runtime dependencies and supports Python 3.4+ and PyPy3.

## Commands

```bash
# Run all tests
pytest -W error

# Run a single test file
pytest tests/test_seqs.py

# Run a single test
pytest tests/test_seqs.py::test_take

# Lint
flake8 funcy
flake8 --select=F,E5,W tests

# Type checking tests (validates stubs against # E: markers)
python type_tests/run.py # run all three checkers
python type_tests/run.py mypy # run a single checker
python type_tests/run.py pyright
python type_tests/run.py ty
python type_tests/run.py coverage # verify all public names have type tests

# Verify stubs match runtime signatures
tox -e stubtest

# Build docs
cd docs && sphinx-build -b html -W . _build/html

# Tox (multi-version testing)
tox -e py313
tox -e lint
tox -e docs
tox -e typetest
tox -e stubtest
```

## Architecture

All public API is re-exported through `funcy/__init__.py` via wildcard imports. Each module defines `__all__`.

### Module Map

- **seqs.py** - Sequence/iterator operations (`take`, `drop`, `first`, `map`, `filter`, `partition`, `chunks`, `group_by`, `distinct`, etc.)
- **colls.py** - Collection manipulation (`merge`, `walk`, `select`, `get_in`, `set_in`, `split_keys`, etc.)
- **funcs.py** - Function composition (`identity`, `partial`, `curry`, `compose`, `complement`, `juxt`)
- **flow.py** - Control flow (`retry`, `throttle`, `ignore`, `silent`, `once`, `limit_error_rate`)
- **calc.py** - Caching/memoization (`memoize`, `cache`, `make_lookuper`)
- **decorators.py** - Decorator utilities (`@decorator`, `@wraps`, `ContextDecorator`)
- **debug.py** - Debugging helpers (`tap`, `log_calls`, `log_errors`, `print_durations`)
- **objects.py** - Object utilities (`cached_property`, `monkey`, `LazyObject`)
- **strings.py** - Regex wrappers (`re_find`, `re_all`, `re_test`)
- **types.py** - Type predicates (`isa`, `is_mapping`, `is_seq`)
- **tree.py** - Tree traversal (`tree_leaves`, `tree_nodes`)
- **funcolls.py** - Functional collection predicates (`all_fn`, `any_fn`, `none_fn`, `some_fn`)
- **funcmakers.py** - Extended function semantics (internal, not in `__all__` of `__init__`)
- **_inspect.py** - Function introspection helpers (internal)

### Key Design Patterns

- **Lazy by default**: Most sequence operations return iterators. Eager variants are prefixed with `l` (e.g., `lmap`, `lfilter`, `lcat`).
- **Extended function protocol** (`funcmakers.py`): Many functions accept not just callables but also regex strings, ints/slices (as `itemgetter`), dicts (as lookup), and sets (as membership test). This is handled by `make_func`/`make_pred`.
- **Type preservation**: Collection operations like `walk` preserve the input type (dict stays dict, set stays set).

### Type Stubs

For type stub details (patterns, `.pyih` code generation, type tests), see [CLAUDE_TYPES.md](CLAUDE_TYPES.md). For extended `.pyih` syntax reference, see [PYIH.md](PYIH.md).

## Shell Rules

- Never use `cat <<EOF` / heredoc in Bash. Use the Write tool to create files instead, even for temporary ones.

## Code Style

- Max line length: 100
- Flake8 with many relaxed rules (see `tox.ini` `[flake8]` section)
- Tests use the `whatever` library for concise lambda-like expressions (`_ + 1`)
36 changes: 36 additions & 0 deletions CLAUDE_TYPES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Type Stubs

**Goal**: Catch type errors and provide useful types on exits, but don't break or annoy users. Types should be precise where possible but never reject valid code.

Type annotations live in `.pyi` stub files, not inline, to keep runtime code clean. The `py.typed` marker (PEP 561) is present.

**Key patterns in stubs**:
- Be as specific as possible with types: use `Callable[[_T], Any]` not `Callable[..., Any]` when a function takes one element arg; use `Callable[[_T, _T], _T]` for reducers, etc.
- Use `TypeVar _T` for type preservation (`empty(coll: _T) -> _T`) instead of redundant overloads
- Use `Callable[[_K], _K2]` for functions that transform types (e.g. `walk_keys`)
- EMPTY sentinel: functions like `all(pred, seq=EMPTY)` that shift args use `@overload`, listed in `stubtest_allowlist.txt`
- Regex return types: `_ReResult = str | tuple[str, ...] | dict[str, str]` — can't narrow by pattern without a mypy plugin (pyright and ty have no plugin systems)

## `.pyih` -> `.pyi` Code Generation

Four modules use `.pyih` source files that are translated to `.pyi` stubs: `seqs`, `colls`, `funcmakers`, `funcs`. The translator is `translate_pyih.py`. **Never directly edit a `.pyi` that has a `.pyih` counterpart** — a repo hook blocks this.

**Workflow**: edit the `.pyih` file, then run `python translate_pyih.py` to regenerate all `.pyi` files.

For full `.pyih` syntax documentation (XFunc, XPred, collection expansion, xfunc_skip, etc.), see [PYIH.md](PYIH.md).

## Type Tests

Type tests live in `type_tests/` at repo root. The runner (`type_tests/run.py`) validates against three checkers (mypy, pyright, ty).

Test file markers:
- No marker: line must type-check cleanly on all checkers
- `# E: <reason>`: line must produce a type error (all checkers must error)
- `# XFAIL: <reason>`: line currently errors but shouldn't ideally (aspirational, all checkers)
- `# XFAIL[ty]: <reason>`: known failure for a specific checker (ty has many TypeVar inference bugs)
- `# E: reason # XFAIL[ty]: reason`: expected error, but ty doesn't catch it (mypy/pyright must error, ty is excused)
- `# R: type # XFAIL[ty]: reason`: expected reveal type, but ty gets it wrong (skips reveal check for ty)

**Testing philosophy**: We care about our stubs working correctly, not about what checkers can infer. When testing with abstract types (Mapping, Sequence, Iterable), use real implementations — custom subclasses — not concrete types cast to abstract (e.g. `m: Mapping = d` where `d` is a dict). Checkers see through such casts and resolve to the concrete type, making the test misleading. A real `class MyMapping(Mapping[K, V])` forces the checker to use the abstract overload.

`stubtest` verifies stubs match runtime signatures. Functions with EMPTY sentinel patterns go in `stubtest_allowlist.txt`.
109 changes: 109 additions & 0 deletions PYIH.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
# `.pyih` → `.pyi` Stub Generation

Funcy uses `.pyih` (pyi higher) source files to generate `.pyi` type stubs. This avoids the cross-product explosion of overloads when typing the extended function protocol (7 variants) combined with collection type preservation.

## Quick Start

```bash
# Edit the .pyih source
vim funcy/seqs.pyih

# Regenerate all .pyi files
python translate_pyih.py

# Verify types
python type_tests/run.py mypy
python type_tests/run.py pyright
python type_tests/run.py ty
```

## Which modules use `.pyih`

| Module | `.pyih` source | Generated `.pyi` |
|--------|---------------|-----------------|
| seqs | `funcy/seqs.pyih` | `funcy/seqs.pyi` |
| colls | `funcy/colls.pyih` | `funcy/colls.pyi` |
| funcs | `funcy/funcs.pyih` | `funcy/funcs.pyi` |
| funcmakers | `funcy/funcmakers.pyih` | `funcy/funcmakers.pyi` |

Other modules (`flow`, `calc`, `debug`, etc.) have hand-written `.pyi` stubs.

## `.pyih` Syntax

`.pyih` files are valid Python 3.12+ syntax. They use three custom constructs that `translate_pyih.py` expands:

### `XFunc[[A], B]` — Extended mapper

Used for function parameters that accept the extended function protocol (Callable, None, Set, Regex, int, slice, Mapping) and transform values from type `A` to type `B`.

```python
def map(f: XFunc[[A], B], seq: Iterable[A]) -> Iterator[B]: ...
```

Expands to 7 overloads:

| Variant | `f` type | `A` becomes | `B` becomes |
|---------|----------|-------------|-------------|
| Callable | `Callable[[A], B]` | A | B |
| None | `None` | A | A (identity) |
| Set | `AbstractSet[A]` | A | `bool` |
| Regex | `str \| bytes \| re.Pattern[str]` | `Any` | `_ReResult \| None` |
| int | `int` | `Sequence[_T]` | `_T` (itemgetter) |
| slice | `slice` | `Sequence[_T]` | `Sequence[_T]` |
| Mapping | `Mapping[A, B]` | A | B (lookup) |

### `XPred[A]` — Extended predicate

Used for function parameters that accept the extended function protocol as a predicate. The element type `A` is preserved (predicates filter, not transform).

```python
def filter(pred: XPred[A], seq: Iterable[A]) -> Iterator[A]: ...
```

Expands to 7 overloads that constrain the input type:

| Variant | `pred` type | `A` constraint |
|---------|------------|----------------|
| Callable | `Callable[[A], Any]` | A |
| None | `None` | A (truthiness) |
| Set | `AbstractSet[A]` | A (membership) |
| Regex | `str \| bytes \| re.Pattern[str]` | `str` |
| int | `int` | `Sequence[Any]` |
| slice | `slice` | `Sequence[Any]` |
| Mapping | `Mapping[A, Any]` | A (key existence) |

### `[C: (list, set, ...)]` — Collection type expansion

PEP 695 type parameter syntax. Generates one overload per concrete type, substituting `C` throughout. Combines with XFunc/XPred for quadratic expansion.

```python
def walk[C: (list, set, frozenset)](f: XFunc[[A], B], coll: C[A]) -> C[B]: ...
```

Generates `3 * 7 = 21` overloads (3 collection types x 7 XFunc variants).

### `# xfunc_skip: Callable` — Skip variants

Placed before a function def. Skips named variant(s) during expansion. Useful when the Callable case needs a separate hand-written overload with more specific types.

```python
# xfunc_skip: Callable
def compose(f: XFunc[[_T], _V]) -> Callable[[_T], _V]: ...
# Callable case handled separately with typed multi-arg overloads:
def compose(__f: Callable[..., _R], *rest: _XFunc) -> Callable[..., _R]: ...
```

### Passthrough

Functions without `XFunc`, `XPred`, or collection type parameters pass through verbatim:

```python
def take(n: int, seq: Iterable[_T]) -> list[_T]: ... # copied as-is
```

## Rules

1. **Never edit `.pyi` files directly** if a `.pyih` counterpart exists — a repo hook blocks this.
2. After editing a `.pyih`, always run `python translate_pyih.py` to regenerate.
3. Run all three type checkers: `python type_tests/run.py mypy && python type_tests/run.py ty && python type_tests/run.py pyright`
4. Run `python -m mypy.stubtest funcy.<module> --allowlist stubtest_allowlist.txt` to verify stubs match runtime.
8 changes: 8 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -184,13 +184,21 @@ To run the tests using your default python:
pip install -r test_requirements.txt
pytest

To run type checking tests::

pip install mypy pyright
python type_tests/run.py mypy
python type_tests/run.py pyright

To fully run ``tox`` you need all the supported pythons to be installed. These are
3.4+ and PyPy3. You can run it for particular environment even in absense
of all of the above::

tox -e py310
tox -e pypy3
tox -e lint
tox -e typetest
tox -e stubtest


.. |Build Status| image:: https://github.com/Suor/funcy/actions/workflows/test.yml/badge.svg
Expand Down
2 changes: 1 addition & 1 deletion docs/extended_fns.rst
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ Sequence filtering :func:`filter` :func:`remove` :func:`distinct`
Sequence splitting :func:`dropwhile` :func:`takewhile` :func:`split` :func:`split_by` :func:`partition_by`
Aggregration :func:`group_by` :func:`count_by` :func:`group_by_keys`
Collection transformation :func:`walk` :func:`walk_keys` :func:`walk_values`
Collection filtering :func:`select` :func:`select_keys` :func:`select_values`
Collection filtering :func:`select` :func:`select_keys` :func:`select_values` :func:`split_keys`
Content tests :func:`all` :func:`any` :func:`none` :func:`one` :func:`some` :func:`is_distinct`
Function logic :func:`all_fn` :func:`any_fn` :func:`none_fn` :func:`one_fn` :func:`some_fn`
Function tools :func:`iffy` :func:`compose` :func:`rcompose` :func:`complement` :func:`juxt` :func:`all_fn` :func:`any_fn` :func:`none_fn` :func:`one_fn` :func:`some_fn`
Expand Down
21 changes: 21 additions & 0 deletions funcy/calc.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from collections.abc import Callable, Mapping
from datetime import timedelta
from typing import Any, TypeVar, overload

__all__ = ['memoize', 'make_lookuper', 'silent_lookuper', 'cache']

_F = TypeVar('_F', bound=Callable[..., Any])
_K = TypeVar('_K')
_V = TypeVar('_V')

class SkipMemory(Exception): ...

@overload
def memoize(func: _F) -> _F: ...
@overload
def memoize(*, key_func: Callable[..., Any]) -> Callable[[_F], _F]: ...

def cache(timeout: int | float | timedelta, *, key_func: Callable[..., Any] | None = ...) -> Callable[[_F], _F]: ...

def make_lookuper(func: Callable[..., Mapping[_K, _V]]) -> Callable[..., _V]: ...
def silent_lookuper(func: Callable[..., Mapping[_K, _V]]) -> Callable[..., _V | None]: ...
Loading
Loading