Skip to content

Commit e7b64b1

Browse files
feat: add python functools reduce (#613)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
1 parent 5c5461c commit e7b64b1

File tree

18 files changed

+1908
-1409
lines changed

18 files changed

+1908
-1409
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ Ideas that will be planned and find their way into a release at one point
2323
- [ ] Java
2424
- [ ] Haskell
2525

26+
## main
27+
28+
- Added a first `python/functools` harvest covering `reduce`.
29+
2630
## v3.0.33
2731

2832
Released: 2026-03-30. [Diff](https://github.com/locutusjs/locutus/compare/v3.0.32...v3.0.33).

docs/non-php-api-signatures.snapshot

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,7 @@ src/python/_helpers/_calendar.ts :: export function normalizeWeekWidth(value: un
283283
src/python/_helpers/_calendar.ts :: export function pythonWeekday(year: number, month: number, day: number): number
284284
src/python/_helpers/_calendar.ts :: export function timeTupleToUnixSeconds( year: number, month: number, day: number, hour: number, minute: number, second: number, ): number
285285
src/python/_helpers/_calendar.ts :: export function toCalendarInteger(value: unknown, functionName: string): number
286+
src/python/_helpers/_functools.ts :: export function pythonReduce( func: (accumulator: unknown, value: unknown) => unknown, iterable: unknown, initializer: unknown = noInitializer, ): unknown
286287
src/python/_helpers/_itertools.ts :: export function itertoolsAccumulate( iterable: unknown, func: ((accumulator: unknown, value: unknown) => unknown) | undefined, ): unknown[]
287288
src/python/_helpers/_itertools.ts :: export function itertoolsBatched(iterable: unknown, n: unknown): unknown[][]
288289
src/python/_helpers/_itertools.ts :: export function itertoolsChain(iterables: unknown[]): unknown[]
@@ -356,6 +357,7 @@ src/python/calendar/weekday.ts :: export function weekday(year: unknown, month:
356357
src/python/calendar/weekheader.ts :: export function weekheader(width: unknown): string
357358
src/python/difflib/get_close_matches.ts :: export function get_close_matches( word: string, possibilities: string[] | unknown, n: number = 3, cutoff: number = 0.6, ): string[]
358359
src/python/difflib/ndiff.ts :: export function ndiff( a: string[] | unknown, b: string[] | unknown, linejunk: LineJunkPredicate = null, charjunk: CharJunkPredicate = isCharacterJunk, ): string[]
360+
src/python/functools/reduce.ts :: export function reduce( func: (accumulator: unknown, value: unknown) => unknown, iterable: unknown, initializer?: unknown, ): unknown
359361
src/python/itertools/accumulate.ts :: export function accumulate(iterable: unknown, func?: (accumulator: unknown, value: unknown) => unknown): unknown[]
360362
src/python/itertools/batched.ts :: export function batched(iterable: unknown, n: unknown): unknown[][]
361363
src/python/itertools/chain.ts :: export function chain(...iterables: unknown[]): unknown[]

docs/prompts/LOG.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3244,3 +3244,29 @@ LLMs log key learnings, progress, and next steps in one `### Iteration ${increme
32443244
- Key learnings:
32453245
- The generated standalone test path will expose symbol collisions that the source/module variants can miss, so alias-like function names need helper internals with distinct names.
32463246
- `bisect` is a good example of a small Python module where strict target-definition work pays off: once the inventory and wishlist are explicit, the harvest becomes a narrow parity exercise rather than guesswork.
3247+
3248+
### Iteration 160
3249+
3250+
2026-03-30
3251+
3252+
- **Area: Product (Python functools harvest 1)**
3253+
- Plan:
3254+
- Ship the smallest high-value `functools` addition first, centered on `reduce`.
3255+
- Keep parity strict by teaching the Python translator only the narrow callback form this harvest actually needs.
3256+
- Progress:
3257+
- Added `src/python/functools/reduce.ts` plus a shared `_functools` helper that preserves Python-style omitted-initializer semantics, including the empty-sequence error path.
3258+
- Added `src/python/functools/index.ts`, updated `src/python/index.ts`, and wired both Rosetta maps so the new function is part of the public surface.
3259+
- Extended `test/parity/lib/languages/python.ts` with a focused `reduce` callback translation path, allowing simple JS arrow reducers like `(a, b) => a + b` to execute as native Python lambdas during parity.
3260+
- Added focused util coverage, generated tests, website pages, and removed the stale `functools/reduce` wishlist entry from the upstream-surface inventory now that it is shipped.
3261+
- Validation:
3262+
- `corepack yarn exec vitest run test/util/python-functools-harvest-1.vitest.ts`
3263+
- `corepack yarn test:parity python/functools/reduce --no-cache`
3264+
- `node scripts/rmrf.ts test/generated && corepack yarn build:tests`
3265+
- `corepack yarn exec vitest run test/generated/python/functools/*.vitest.ts`
3266+
- `corepack yarn injectweb && corepack yarn website:verify`
3267+
- `corepack yarn fix:api:snapshot:nonphp`
3268+
- `corepack yarn fix:type:contracts`
3269+
- `corepack yarn check`
3270+
- Key learnings:
3271+
- Higher-order APIs need parity-safe examples or translator support; `reduce` is a good case where a tiny callback bridge is cleaner than contorting the docs.
3272+
- For shipped backlog items, the upstream-surface inventory needs the corresponding `wanted` decision removed immediately or CI will correctly flag stale policy drift.

docs/upstream-surface-inventory.yml

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1487,10 +1487,6 @@ python:
14871487
default:
14881488
decision: skip_runtime_model
14891489
note: Most functools exports are decorators, wrappers, or callable factories rather than plain values.
1490-
decisions:
1491-
reduce:
1492-
decision: wanted
1493-
note: Left-folding over plain iterables is a strong portability target.
14941490
itertools:
14951491
title: itertools module
14961492
default:

src/python/_helpers/_functools.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
export const noInitializer = Symbol('noInitializer')
2+
3+
export function pythonReduce(
4+
func: (accumulator: unknown, value: unknown) => unknown,
5+
iterable: unknown,
6+
initializer: unknown = noInitializer,
7+
): unknown {
8+
const iterator = toIterator(iterable, 'reduce')
9+
let accumulator: unknown
10+
11+
if (initializer !== noInitializer) {
12+
accumulator = initializer
13+
} else {
14+
const first = iterator.next()
15+
if (first.done) {
16+
throw new TypeError('reduce() of empty sequence with no initial value')
17+
}
18+
accumulator = first.value
19+
}
20+
21+
while (true) {
22+
const next = iterator.next()
23+
if (next.done) {
24+
return accumulator
25+
}
26+
27+
accumulator = func(accumulator, next.value)
28+
}
29+
}
30+
31+
function toIterator(iterable: unknown, functionName: string): Iterator<unknown> {
32+
if (typeof iterable === 'string') {
33+
return iterable[Symbol.iterator]()
34+
}
35+
36+
if ((typeof iterable === 'object' || typeof iterable === 'function') && iterable !== null) {
37+
const iterableValue = iterable as {
38+
[Symbol.iterator]?: () => Iterator<unknown>
39+
}
40+
const iteratorFactory = iterableValue[Symbol.iterator]
41+
42+
if (typeof iteratorFactory === 'function') {
43+
return iteratorFactory.call(iterableValue)
44+
}
45+
}
46+
47+
throw new TypeError(`${functionName}() expected an iterable`)
48+
}

src/python/functools/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { reduce } from './reduce.ts'

src/python/functools/reduce.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { noInitializer, pythonReduce } from '../_helpers/_functools.ts'
2+
3+
export function reduce(
4+
func: (accumulator: unknown, value: unknown) => unknown,
5+
iterable: unknown,
6+
initializer?: unknown,
7+
): unknown {
8+
// discuss at: https://locutus.io/python/functools/reduce/
9+
// parity verified: Python 3.12
10+
// original by: Kevin van Zonneveld (https://kvz.io)
11+
// note 1: Reduces Python iterables to one plain value using a caller-provided combining function.
12+
// example 1: reduce((a, b) => a + b, [1, 2, 3, 4])
13+
// returns 1: 10
14+
// example 2: reduce((a, b) => a + b, 'abc')
15+
// returns 2: 'abc'
16+
17+
return arguments.length >= 3 ? pythonReduce(func, iterable, initializer) : pythonReduce(func, iterable, noInitializer)
18+
}

src/python/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
export * as bisect from './bisect/index.ts'
22
export * as calendar from './calendar/index.ts'
33
export * as difflib from './difflib/index.ts'
4+
export * as functools from './functools/index.ts'
45
export * as itertools from './itertools/index.ts'
56
export * as math from './math/index.ts'
67
export * as operator from './operator/index.ts'

src/rosetta.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -353,6 +353,9 @@ sequence_interpose:
353353
enumerable_reduce_while:
354354
- elixir/Enum/reduce_while
355355

356+
enumerable_reduce:
357+
- python/functools/reduce
358+
356359
enumerable_zip:
357360
- ruby/Array/zip
358361
- elixir/Enum/zip
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
// warning: This file is auto generated by `yarn build:tests`
2+
// Do not edit by hand!
3+
4+
import { createRequire } from 'node:module'
5+
import { describe, it, expect } from 'vitest'
6+
7+
process.env.TZ = 'UTC'
8+
const __locutus_source_fn = require('../../../../src/python/functools/reduce.ts').reduce
9+
const __locutus_source_module_url = new URL("../../../../src/python/functools/reduce.ts", import.meta.url)
10+
const __locutus_source_require = createRequire(__locutus_source_module_url)
11+
const __locutus_func_name = "reduce"
12+
const __locutus_module_js_code = "\"use strict\";\nObject.defineProperty(exports, \"__esModule\", { value: true });\nexports.reduce = reduce;\nconst _functools_ts_1 = require(\"../_helpers/_functools.ts\");\nfunction reduce(func, iterable, initializer) {\n // discuss at: https://locutus.io/python/functools/reduce/\n // parity verified: Python 3.12\n // original by: Kevin van Zonneveld (https://kvz.io)\n // note 1: Reduces Python iterables to one plain value using a caller-provided combining function.\n // example 1: reduce((a, b) => a + b, [1, 2, 3, 4])\n // returns 1: 10\n // example 2: reduce((a, b) => a + b, 'abc')\n // returns 2: 'abc'\n return arguments.length >= 3 ? (0, _functools_ts_1.pythonReduce)(func, iterable, initializer) : (0, _functools_ts_1.pythonReduce)(func, iterable, _functools_ts_1.noInitializer);\n}"
13+
const __locutus_standalone_ts_code = "// python/_helpers/_functools (Locutus helper dependency)\nconst noInitializer = Symbol('noInitializer');\nfunction pythonReduce(func, iterable, initializer = noInitializer) {\n const iterator = toIterator(iterable, 'reduce');\n let accumulator;\n if (initializer !== noInitializer) {\n accumulator = initializer;\n }\n else {\n const first = iterator.next();\n if (first.done) {\n throw new TypeError('reduce() of empty sequence with no initial value');\n }\n accumulator = first.value;\n }\n while (true) {\n const next = iterator.next();\n if (next.done) {\n return accumulator;\n }\n accumulator = func(accumulator, next.value);\n }\n}\nfunction toIterator(iterable, functionName) {\n if (typeof iterable === 'string') {\n return iterable[Symbol.iterator]();\n }\n if ((typeof iterable === 'object' || typeof iterable === 'function') && iterable !== null) {\n const iterableValue = iterable;\n const iteratorFactory = iterableValue[Symbol.iterator];\n if (typeof iteratorFactory === 'function') {\n return iteratorFactory.call(iterableValue);\n }\n }\n throw new TypeError(`${functionName}() expected an iterable`);\n}\n// python/functools/reduce (target function module)\nfunction reduce(func, iterable, initializer) {\n // discuss at: https://locutus.io/python/functools/reduce/\n // parity verified: Python 3.12\n // original by: Kevin van Zonneveld (https://kvz.io)\n // note 1: Reduces Python iterables to one plain value using a caller-provided combining function.\n // example 1: reduce((a, b) => a + b, [1, 2, 3, 4])\n // returns 1: 10\n // example 2: reduce((a, b) => a + b, 'abc')\n // returns 2: 'abc'\n return arguments.length >= 3 ? pythonReduce(func, iterable, initializer) : pythonReduce(func, iterable, noInitializer);\n}"
14+
const __locutus_standalone_js_code = "// python/_helpers/_functools (Locutus helper dependency)\nconst noInitializer = Symbol('noInitializer');\nfunction pythonReduce(func, iterable, initializer = noInitializer) {\n const iterator = toIterator(iterable, 'reduce');\n let accumulator;\n if (initializer !== noInitializer) {\n accumulator = initializer;\n }\n else {\n const first = iterator.next();\n if (first.done) {\n throw new TypeError('reduce() of empty sequence with no initial value');\n }\n accumulator = first.value;\n }\n while (true) {\n const next = iterator.next();\n if (next.done) {\n return accumulator;\n }\n accumulator = func(accumulator, next.value);\n }\n}\nfunction toIterator(iterable, functionName) {\n if (typeof iterable === 'string') {\n return iterable[Symbol.iterator]();\n }\n if ((typeof iterable === 'object' || typeof iterable === 'function') && iterable !== null) {\n const iterableValue = iterable;\n const iteratorFactory = iterableValue[Symbol.iterator];\n if (typeof iteratorFactory === 'function') {\n return iteratorFactory.call(iterableValue);\n }\n }\n throw new TypeError(`${functionName}() expected an iterable`);\n}\n// python/functools/reduce (target function module)\nfunction reduce(func, iterable, initializer) {\n // discuss at: https://locutus.io/python/functools/reduce/\n // parity verified: Python 3.12\n // original by: Kevin van Zonneveld (https://kvz.io)\n // note 1: Reduces Python iterables to one plain value using a caller-provided combining function.\n // example 1: reduce((a, b) => a + b, [1, 2, 3, 4])\n // returns 1: 10\n // example 2: reduce((a, b) => a + b, 'abc')\n // returns 2: 'abc'\n return arguments.length >= 3 ? pythonReduce(func, iterable, initializer) : pythonReduce(func, iterable, noInitializer);\n}"
15+
16+
const __locutus_eval_function = (compiledCode: string): ((...args: unknown[]) => unknown) => {
17+
const evaluator = new Function('require', compiledCode + '\nreturn ' + __locutus_func_name + ';')
18+
return evaluator(__locutus_source_require) as (...args: unknown[]) => unknown
19+
}
20+
const __locutus_eval_module_export = (compiledCode: string, exportName: string): ((...args: unknown[]) => unknown) => {
21+
const module = { exports: {} as { [key: string]: unknown } }
22+
const exports = module.exports
23+
const evaluator = new Function('exports', 'module', 'require', compiledCode)
24+
evaluator(exports, module, __locutus_source_require)
25+
return module.exports[exportName] as (...args: unknown[]) => unknown
26+
}
27+
const __locutus_module_js_fn = __locutus_eval_module_export(__locutus_module_js_code, __locutus_func_name)
28+
const __locutus_standalone_ts_fn = __locutus_eval_function(__locutus_standalone_ts_code)
29+
const __locutus_standalone_js_fn = __locutus_eval_function(__locutus_standalone_js_code)
30+
31+
describe('src/python/functools/reduce.ts (tested in test/generated/python/functools/reduce.vitest.ts)', function () {
32+
it('should pass example 1', function () {
33+
const expected = 10
34+
const __locutus_variants: Array<{ name: string; fn: (...args: unknown[]) => unknown }> = [
35+
{ name: "source", fn: __locutus_source_fn },
36+
{ name: "module-js", fn: __locutus_module_js_fn },
37+
{ name: "standalone-ts", fn: __locutus_standalone_ts_fn },
38+
{ name: "standalone-js", fn: __locutus_standalone_js_fn },
39+
]
40+
const __locutus_run_example = (reduce: typeof __locutus_source_fn) => {
41+
return reduce((a, b) => a + b, [1, 2, 3, 4])
42+
}
43+
for (const __locutus_variant of __locutus_variants) {
44+
const result = __locutus_run_example(__locutus_variant.fn as typeof __locutus_source_fn)
45+
expect(result).toEqual(expected)
46+
}
47+
})
48+
it('should pass example 2', function () {
49+
const expected = 'abc'
50+
const __locutus_variants: Array<{ name: string; fn: (...args: unknown[]) => unknown }> = [
51+
{ name: "source", fn: __locutus_source_fn },
52+
{ name: "module-js", fn: __locutus_module_js_fn },
53+
{ name: "standalone-ts", fn: __locutus_standalone_ts_fn },
54+
{ name: "standalone-js", fn: __locutus_standalone_js_fn },
55+
]
56+
const __locutus_run_example = (reduce: typeof __locutus_source_fn) => {
57+
return reduce((a, b) => a + b, 'abc')
58+
}
59+
for (const __locutus_variant of __locutus_variants) {
60+
const result = __locutus_run_example(__locutus_variant.fn as typeof __locutus_source_fn)
61+
expect(result).toEqual(expected)
62+
}
63+
})
64+
})

0 commit comments

Comments
 (0)