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
9 changes: 9 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
# **py2be** Changes


## 0.1.0 - 2nd July 2026

* modularised implementation (`constants`, `parse`, `truthy`);
* added **benchmarks/string_truthy.py** and **benchmarks/run_all_benchmarks.sh**;
* `str2bool()` and `string_is_truthy()` now classify stock terms via `_TRUTHY_STRINGS` and a module-built `_TRUTHY_TABLE` keyed by `(length, first_char)` (cf. **to-be.Rust** first-letter dispatch); always strips before lookup; strong on padded stock terms and unrecognised inputs (benchmarked);
* retained one-sided `string_is_falsey()` and `string_is_truey()` paths (`_str_is_falsey`, `_str_is_truey`) with opposite-precise fast-fail and conditional `strip().lower()` (cf. **to-be.Rust**); `constants.py` precise/lowercase tuples remain for these paths until a later elision pass;
* added docstrings to `str2bool()`, `string_is_falsey()`, `string_is_truey()`, and `string_is_truthy()`;
* measured and ruled out for stock vocabulary at current table sizes: merged precise-table linear scan, `bisect` lookup, state-machine parsers, and conditional-trim fast paths (see **README** benchmarks section);

## 0.0.4 - 2nd July 2026

* added top-level `__all__` documenting the public API;
Expand Down
1 change: 1 addition & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
include LICENSE README.md CHANGES.md EXAMPLES.md TODO.md
recursive-include benchmarks *.py
recursive-include examples *.py
recursive-include tests *.py
24 changes: 23 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ Simple Python library determining whether strings indicate *truey* or *falsey* v
- [Terminology](#terminology)
- [Components](#components)
- [Functions](#functions)
- [Benchmarks](#benchmarks)
- [Examples](#examples)
- [Project Information](#project-information)
- [Where to get help](#where-to-get-help)
Expand Down Expand Up @@ -154,11 +155,32 @@ The following public functions are defined in the current version:
Stock falsey terms (after optional trimming and case folding) include `0`, `false`, `no`, and `off`. Stock truey terms include `1`, `true`, `yes`, and `on`. Several common capitalisations and mixtures of case are recognised without lower-casing first.


## Benchmarks

Benchmark scripts are provided under `benchmarks/`, modelled on **to-be.Rust**'s `string_truthy` Criterion suite.

Install the package (or set `PYTHONPATH=.` from the repository root), then run:

```
$ pip install -e .
$ ./benchmarks/run_all_benchmarks.sh
```

To run a subset of groups:

```
$ python benchmarks/string_truthy.py string_is_truthy
$ python benchmarks/string_truthy.py mixed_batch
```

Run benchmarks on mains power for stable timings. On AC power (Apple Silicon, CPython 3.9, `number=200000`, `repeat=5`), one-sided `string_is_falsey()` / `string_is_truey()` paths are roughly **30–40% faster** on matching stock terms and **~12% faster** on unrecognised inputs versus routing both through full `_str2bool()`; `string_is_truthy()` is unchanged. Cross-polarity use (e.g. `string_is_truey("false")`) may be slower — use `str2bool()` or `string_is_truthy()` when full classification is needed.


## Examples

Examples are provided in the `examples` directory. A detailed list of them is provided in [EXAMPLES.md](./EXAMPLES.md).

To run the stock string classification example:
To run the stock string classification example (after `pip install -e .`):

```
$ python examples/truthy_strings.py
Expand Down
7 changes: 5 additions & 2 deletions TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,15 @@

## Functional improvements

* \<none>
* [ ] `Terms`, `string_is_truthy_with()`, and `stock_term_strings()` (cf. **to-be.Rust**);


## Performance improvements

* \<none>
* [x] **benchmarks/string_truthy.py** and **benchmarks/run_all_benchmarks.sh**;
* [x] one-sided `string_is_falsey()` / `string_is_truey()` paths with opposite-precise fast-fail (cf. **to-be.Rust**);
* [-] ~~~conditional-trim fast path (cf. **to-be.Rust**; measured regression on CPython for stock terms)~~~;
* [-] ~~~`bisect` precise-table lookup (cf. **to-be.Rust**; measured regression at current stock table sizes)~~~;


<!-- ########################### end of file ########################### -->
Expand Down
9 changes: 9 additions & 0 deletions benchmarks/run_all_benchmarks.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#! /bin/bash

set -e

cd "$(dirname "$0")/.."

export PYTHONPATH="${PYTHONPATH:+$PYTHONPATH:}$(pwd)"

python benchmarks/string_truthy.py "$@"
188 changes: 188 additions & 0 deletions benchmarks/string_truthy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
#! /usr/bin/env python
# -*- coding: utf-8 -*-
"""
Benchmark string truthy evaluation (stock terms).

Run from the repository root with py2be installed or on PYTHONPATH:

python benchmarks/string_truthy.py

Filter benchmark groups:

python benchmarks/string_truthy.py string_is_truthy
python benchmarks/string_truthy.py mixed_batch
"""

from __future__ import print_function

import sys
import timeit

from py2be import (
string_is_falsey,
string_is_truey,
string_is_truthy,
)


INPUTS = (
('true', 'lower'),
('TRUE', 'upper'),
('True', 'title'),
('false', 'lower'),
('FALSE', 'upper'),
('False', 'title'),
('yes', 'lower'),
('YES', 'upper'),
('Yes', 'title'),
('no', 'lower'),
('NO', 'upper'),
('No', 'title'),
('on', 'lower'),
('ON', 'upper'),
('On', 'title'),
('off', 'lower'),
('OFF', 'upper'),
('Off', 'title'),
('1', ''),
('0', ''),
('unrecognised', 'unrecognised'),
)


PADDED_INPUTS = (
(' true', 'leading/lower/true'),
('true ', 'trailing/lower/true'),
(' true ', 'both/lower/true'),
(' FALSE', 'leading/upper/FALSE'),
('false ', 'trailing/lower/false'),
(' YES ', 'both/upper/YES'),
(' 1', 'leading/1'),
('0 ', 'trailing/0'),
(' unrecognised ', 'both/unrecognised'),
)


MIXED_INPUTS = (
'yes',
'no',
'TRUE',
'off',
'maybe',
'1',
'0',
'',
)

MIXED_PADDED_INPUTS = (
' yes ',
' no ',
' TRUE ',
' off ',
' maybe ',
' 1 ',
' 0 ',
' ',
)


NUMBER = 200000
REPEAT = 5


def _bench_name(label, input_value):

if label:
return '%s/%s' % (label, input_value)
return input_value


def _run_bench(name, fn, arg):

elapsed = min(timeit.repeat(lambda: fn(arg), repeat=REPEAT, number=NUMBER))
per_op_ns = (elapsed / float(NUMBER)) * 1e9

print('%-48s %8.1f ns/op' % (name, per_op_ns))


def _run_mixed(name, fn, inputs):

def batch():

for s in inputs:
fn(s)

elapsed = min(timeit.repeat(batch, repeat=REPEAT, number=NUMBER))
per_op_ns = (elapsed / float(NUMBER)) * 1e9

print('%-48s %8.1f ns/op' % (name, per_op_ns))


def bench_group(group_name, inputs, classify):

print('[%s]' % group_name)

for input_value, label in inputs:
name = '%s/%s' % (group_name, _bench_name(label, input_value))
_run_bench(name, classify, input_value)

print('')


def bench_mixed(group_name, inputs, classify):

print('[%s]' % group_name)
_run_mixed('%s/mixed_batch' % group_name, classify, inputs)
print('')


def main(argv):

groups = {
'string_is_truthy': (
lambda: bench_group('string_is_truthy', INPUTS, string_is_truthy),
lambda: bench_group('string_is_truthy_padded', PADDED_INPUTS, string_is_truthy),
lambda: bench_mixed('string_is_truthy', MIXED_INPUTS, string_is_truthy),
lambda: bench_mixed('string_is_truthy_padded', MIXED_PADDED_INPUTS, string_is_truthy),
),
'string_is_truey': (
lambda: bench_group('string_is_truey', INPUTS, string_is_truey),
lambda: bench_group('string_is_truey_padded', PADDED_INPUTS, string_is_truey),
),
'string_is_falsey': (
lambda: bench_group('string_is_falsey', INPUTS, string_is_falsey),
lambda: bench_group('string_is_falsey_padded', PADDED_INPUTS, string_is_falsey),
),
'mixed_batch': (
lambda: bench_mixed('string_is_truthy', MIXED_INPUTS, string_is_truthy),
lambda: bench_mixed('string_is_truthy_padded', MIXED_PADDED_INPUTS, string_is_truthy),
),
}

selected = argv[1:]

if not selected:
selected = [
'string_is_truthy',
'string_is_truey',
'string_is_falsey',
'mixed_batch',
]

print('py2be string truthy benchmarks (number=%d, repeat=%d)' % (NUMBER, REPEAT))
print('')

for key in selected:
if key not in groups:
print('Unknown group: %s' % key, file=sys.stderr)
return 1

for run in groups[key]:
run()

return 0


if '__main__' == __name__:

sys.exit(main(sys.argv))
3 changes: 1 addition & 2 deletions py2be/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
__license__ = 'BSD-3-Clause'
__maintainer__ = 'Matt Wilson'
__status__ = 'Beta'
__version__ = '0.0.4'
__version__ = '0.1.0'

from .truthy import (
str2bool,
Expand All @@ -29,4 +29,3 @@


# ############################## end of file ############################# #

40 changes: 40 additions & 0 deletions py2be/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@

FALSEY_PRECISE_STRINGS = (
"0",
"FALSE",
"False",
"NO",
"No",
"OFF",
"Off",
"false",
"no",
"off",
)

TRUEY_PRECISE_STRINGS = (
"1",
"ON",
"On",
"TRUE",
"True",
"YES",
"Yes",
"on",
"true",
"yes",
)

FALSEY_LOWERCASE_STRINGS = (
"false",
"no",
"off",
"0",
)

TRUEY_LOWERCASE_STRINGS = (
"true",
"yes",
"on",
"1",
)
Loading
Loading