From 60a95f93ca66d18df5943dd78d75fd93c0359fc4 Mon Sep 17 00:00:00 2001 From: Sam Fakhreddine Date: Tue, 17 Oct 2023 13:47:31 -0600 Subject: [PATCH 01/27] Update setup.py --- setup.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/setup.py b/setup.py index 4687965..3dd720b 100644 --- a/setup.py +++ b/setup.py @@ -15,10 +15,11 @@ 'Development Status :: 5 - Production/Stable', 'License :: OSI Approved :: BSD License', 'Operating System :: OS Independent', - 'Programming Language :: Python :: 2', - 'Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', + 'Programming Language :: Python :: 3.10', + 'Programming Language :: Python :: 3.11', + 'Programming Language :: Python :: 3.12', 'Topic :: Utilities' ], ) From 51e1b466853fba5e764b74bc7474231096d7103c Mon Sep 17 00:00:00 2001 From: Sam Fakhreddine Date: Tue, 17 Oct 2023 13:53:08 -0600 Subject: [PATCH 02/27] Update __init__.py --- str2bool/__init__.py | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/str2bool/__init__.py b/str2bool/__init__.py index 4dc0017..41b968e 100644 --- a/str2bool/__init__.py +++ b/str2bool/__init__.py @@ -1,21 +1,22 @@ -import sys +from typing import Union -_true_set = {'yes', 'true', 't', 'y', '1'} -_false_set = {'no', 'false', 'f', 'n', '0'} +# Refactored Python 3.9 compatible code for string to boolean conversion - -def str2bool(value, raise_exc=False): - if isinstance(value, str) or sys.version_info[0] < 3 and isinstance(value, basestring): +def str2bool(value: Union[str, None], raise_exc: bool = False) -> Union[bool, None]: + true_set = {'yes', 'true', 't', 'y', '1'} + false_set = {'no', 'false', 'f', 'n', '0'} + + if isinstance(value, str): value = value.lower() - if value in _true_set: + if value in true_set: return True - if value in _false_set: + if value in false_set: return False - + if raise_exc: - raise ValueError('Expected "%s"' % '", "'.join(_true_set | _false_set)) + raise ValueError(f'Expected one of: {", ".join(true_set | false_set)}') + return None - -def str2bool_exc(value): +def str2bool_exc(value: Union[str, None]) -> bool: return str2bool(value, raise_exc=True) From 477423b163aa9b6e05288c642e8ba07dadbfca5b Mon Sep 17 00:00:00 2001 From: Sam Fakhreddine Date: Tue, 17 Oct 2023 13:57:51 -0600 Subject: [PATCH 03/27] Update setup.py --- setup.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/setup.py b/setup.py index 3dd720b..dc2bad4 100644 --- a/setup.py +++ b/setup.py @@ -23,3 +23,29 @@ 'Topic :: Utilities' ], ) + + +from setuptools import setup # Prefer setuptools over distutils + +setup( + name='str2bool3', + packages=['str2bool3'], + version='1.0.0', + description='Convert string to boolean (Forked from SymonSoft/str2bool)', + author='Sam Fakhreddine', + author_email='sam.fakhreddine@gmail.com', + url='https://github.com/sam-fakhreddine/str2bool3', + download_url='https://github.com/sam-fakhreddine/str2bool3/tarball/1.0.0', + keywords=['str2bool', 'bool', 'boolean', 'convert', 'yes', 'no', 'true', 'false'], + classifiers=[ + 'Development Status :: 5 - Production/Stable', + 'License :: OSI Approved :: BSD License', + 'Operating System :: OS Independent', + 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', + 'Programming Language :: Python :: 3.10', + 'Programming Language :: Python :: 3.11', + 'Programming Language :: Python :: 3.12', + 'Topic :: Utilities' + ], +) From 9e7fb699df747a43d94c493bc4f5233919c1c32d Mon Sep 17 00:00:00 2001 From: Sam Fakhreddine Date: Tue, 17 Oct 2023 13:58:04 -0600 Subject: [PATCH 04/27] Update setup.py --- setup.py | 27 --------------------------- 1 file changed, 27 deletions(-) diff --git a/setup.py b/setup.py index dc2bad4..6c05349 100644 --- a/setup.py +++ b/setup.py @@ -1,30 +1,3 @@ -from distutils.core import setup - - -setup( - name='str2bool', - packages=['str2bool'], - version='1.1', - description='Convert string to boolean', - author='SymonSoft', - author_email='symonsoft@gmail.com', - url='https://github.com/symonsoft/str2bool', - download_url='https://github.com/symonsoft/str2bool/tarball/1.1', - keywords=['str2bool', 'bool', 'boolean', 'convert', 'yes', 'no', 'true', 'false'], - classifiers=[ - 'Development Status :: 5 - Production/Stable', - 'License :: OSI Approved :: BSD License', - 'Operating System :: OS Independent', - 'Programming Language :: Python :: 3.8', - 'Programming Language :: Python :: 3.9', - 'Programming Language :: Python :: 3.10', - 'Programming Language :: Python :: 3.11', - 'Programming Language :: Python :: 3.12', - 'Topic :: Utilities' - ], -) - - from setuptools import setup # Prefer setuptools over distutils setup( From 63499e6ca38df022bd0c91b5c04b638e0f47561f Mon Sep 17 00:00:00 2001 From: Sam Fakhreddine Date: Tue, 17 Oct 2023 14:05:42 -0600 Subject: [PATCH 05/27] Update setup.py --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 6c05349..b5c5272 100644 --- a/setup.py +++ b/setup.py @@ -8,7 +8,7 @@ author='Sam Fakhreddine', author_email='sam.fakhreddine@gmail.com', url='https://github.com/sam-fakhreddine/str2bool3', - download_url='https://github.com/sam-fakhreddine/str2bool3/tarball/1.0.0', + download_url='https://github.com/sam-fakhreddine/str2bool3/archive/refs/tags/1.0.0.tar.gz', keywords=['str2bool', 'bool', 'boolean', 'convert', 'yes', 'no', 'true', 'false'], classifiers=[ 'Development Status :: 5 - Production/Stable', From cdac3b9cef2bd0f5b94cd7ad5456ab324eea7651 Mon Sep 17 00:00:00 2001 From: Sam Fakhreddine Date: Tue, 17 Oct 2023 14:16:48 -0600 Subject: [PATCH 06/27] Create workflows.yml --- .github/workflows/workflows.yml | 1 + 1 file changed, 1 insertion(+) create mode 100644 .github/workflows/workflows.yml diff --git a/.github/workflows/workflows.yml b/.github/workflows/workflows.yml new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/.github/workflows/workflows.yml @@ -0,0 +1 @@ + From 6b4b09d085c8bb1490d64761dd1eb265852535a1 Mon Sep 17 00:00:00 2001 From: Sam Fakhreddine Date: Tue, 17 Oct 2023 14:43:38 -0600 Subject: [PATCH 07/27] Update setup.py --- setup.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/setup.py b/setup.py index b5c5272..03d996f 100644 --- a/setup.py +++ b/setup.py @@ -1,14 +1,18 @@ -from setuptools import setup # Prefer setuptools over distutils +import os +from setuptools import setup + +# Read version from environment variable, with fallback to default version +version = os.environ.get('PACKAGE_VERSION', '1.0.0') setup( name='str2bool3', - packages=['str2bool3'], - version='1.0.0', + packages=['str2bool3'], + version=version, description='Convert string to boolean (Forked from SymonSoft/str2bool)', - author='Sam Fakhreddine', - author_email='sam.fakhreddine@gmail.com', + author='Your Name', + author_email='your.email@example.com', url='https://github.com/sam-fakhreddine/str2bool3', - download_url='https://github.com/sam-fakhreddine/str2bool3/archive/refs/tags/1.0.0.tar.gz', + download_url=f'https://github.com/sam-fakhreddine/str2bool3/archive/refs/tags/{version}.tar.gz', keywords=['str2bool', 'bool', 'boolean', 'convert', 'yes', 'no', 'true', 'false'], classifiers=[ 'Development Status :: 5 - Production/Stable', From 1f2e4c354b91a7a689aba5131ebffb203cd0ea4b Mon Sep 17 00:00:00 2001 From: Sam Fakhreddine Date: Tue, 17 Oct 2023 14:44:40 -0600 Subject: [PATCH 08/27] Update workflows.yml --- .github/workflows/workflows.yml | 54 +++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/.github/workflows/workflows.yml b/.github/workflows/workflows.yml index 8b13789..b09c884 100644 --- a/.github/workflows/workflows.yml +++ b/.github/workflows/workflows.yml @@ -1 +1,55 @@ +name: Publish Python 🐍 distribution πŸ“¦ to PyPI and TestPyPI + +on: + push: + tags: + - '*' + +jobs: + build: + name: Build distribution πŸ“¦ + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: '3.x' + + - name: Extract version from git tag + run: echo "PACKAGE_VERSION=${{GITHUB_REF#refs/tags/}}" >> $GITHUB_ENV + + - name: Install pypa/build + run: python3 -m pip install build --user + - name: Build a binary wheel and a source tarball + run: python3 -m build + - name: Store the distribution packages + uses: actions/upload-artifact@v2 + with: + name: python-package-distributions + path: dist/ + + publish-to-pypi: + name: Publish Python 🐍 distribution πŸ“¦ to PyPI + if: startsWith(github.ref, 'refs/tags/') + needs: + - build + runs-on: ubuntu-latest + environment: + name: pypi + url: https://pypi.org/p/str2bool3 + permissions: + id-token: write + + steps: + - name: Download all the dists + uses: actions/download-artifact@v2 + with: + name: python-package-distributions + path: dist/ + - name: Publish distribution πŸ“¦ to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + user: __token__ + password: ${{ secrets.PYPI_API_TOKEN }} From 1d49df7bd7e1528a302fe935147df6c15292e2ac Mon Sep 17 00:00:00 2001 From: Sam Fakhreddine Date: Tue, 17 Oct 2023 14:53:11 -0600 Subject: [PATCH 09/27] Update workflows.yml --- .github/workflows/workflows.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/workflows.yml b/.github/workflows/workflows.yml index b09c884..80271a2 100644 --- a/.github/workflows/workflows.yml +++ b/.github/workflows/workflows.yml @@ -17,7 +17,8 @@ jobs: python-version: '3.x' - name: Extract version from git tag - run: echo "PACKAGE_VERSION=${{GITHUB_REF#refs/tags/}}" >> $GITHUB_ENV + run: echo "PACKAGE_VERSION=${{ github.ref_name }}" >> $GITHUB_ENV + - name: Install pypa/build run: python3 -m pip install build --user From 1dc5ffe3680a666ccabb72207eaddfd793558138 Mon Sep 17 00:00:00 2001 From: Sam Fakhreddine Date: Tue, 17 Oct 2023 14:57:00 -0600 Subject: [PATCH 10/27] Create __init__.py --- str2bool3/__init__.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 str2bool3/__init__.py diff --git a/str2bool3/__init__.py b/str2bool3/__init__.py new file mode 100644 index 0000000..4b90f67 --- /dev/null +++ b/str2bool3/__init__.py @@ -0,0 +1,22 @@ +from typing import Union + +# Refactored Python 3.9 compatible code for string to boolean conversion + +def str2bool(value: Union[str, None], raise_exc: bool = False) -> Union[bool, None]: + true_set = {'yes', 'true', 't', 'y', '1'} + false_set = {'no', 'false', 'f', 'n', '0'} + + if isinstance(value, str): + value = value.lower() + if value in true_set: + return True + if value in false_set: + return False + + if raise_exc: + raise ValueError(f'Expected one of: {", ".join(true_set | false_set)}') + + return None + +def str2bool_exc(value: Union[str, None]) -> bool: + return str2bool(value, raise_exc=True) From 33e9481c33c90a91905b0206cc914d1776cd0cd2 Mon Sep 17 00:00:00 2001 From: Sam Fakhreddine Date: Tue, 17 Oct 2023 14:57:20 -0600 Subject: [PATCH 11/27] Delete str2bool directory --- str2bool/__init__.py | 22 ---------------------- 1 file changed, 22 deletions(-) delete mode 100644 str2bool/__init__.py diff --git a/str2bool/__init__.py b/str2bool/__init__.py deleted file mode 100644 index 41b968e..0000000 --- a/str2bool/__init__.py +++ /dev/null @@ -1,22 +0,0 @@ -from typing import Union - -# Refactored Python 3.9 compatible code for string to boolean conversion - -def str2bool(value: Union[str, None], raise_exc: bool = False) -> Union[bool, None]: - true_set = {'yes', 'true', 't', 'y', '1'} - false_set = {'no', 'false', 'f', 'n', '0'} - - if isinstance(value, str): - value = value.lower() - if value in true_set: - return True - if value in false_set: - return False - - if raise_exc: - raise ValueError(f'Expected one of: {", ".join(true_set | false_set)}') - - return None - -def str2bool_exc(value: Union[str, None]) -> bool: - return str2bool(value, raise_exc=True) From 85888e79f876d94f4b2f48a797075a01e8cef09c Mon Sep 17 00:00:00 2001 From: Sam Fakhreddine Date: Tue, 17 Oct 2023 14:58:19 -0600 Subject: [PATCH 12/27] Update setup.cfg --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index a2f3748..20ce750 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,2 +1,2 @@ [metadata] -description-file = README.md \ No newline at end of file +description_file = README.md From 6ab0d87d6aed48b0919c29f8fa8d5fcaf1fac2d0 Mon Sep 17 00:00:00 2001 From: Sam Fakhreddine Date: Tue, 17 Oct 2023 14:59:16 -0600 Subject: [PATCH 13/27] Update README.md --- README.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 001f470..6561d2b 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# str2bool v.1.1 +# str2bool3 v1.0.0 ## About Convert string to boolean. @@ -7,14 +7,16 @@ Case insensitive. ## Installation - $ pip install str2bool + $ pip install str2bool3 ## Examples Here's a basic example: - >>> from str2bool import str2bool - >>> print(str2bool('Yes')) + >>> from str2bool3 import str2bool3 + >>> print(str2bool3('Yes')) True ## License BSD + +forked from symonsoft/str2bool From f3fc1893a5a5a0d27e073365195289fe727a4d7e Mon Sep 17 00:00:00 2001 From: Sam Fakhreddine Date: Tue, 17 Oct 2023 15:07:55 -0600 Subject: [PATCH 14/27] Update setup.py --- setup.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 03d996f..863ebe8 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,9 @@ # Read version from environment variable, with fallback to default version version = os.environ.get('PACKAGE_VERSION', '1.0.0') - +with open("README.md", "r", encoding="utf-8") as f: + long_description = f.read() + setup( name='str2bool3', packages=['str2bool3'], @@ -12,6 +14,8 @@ author='Your Name', author_email='your.email@example.com', url='https://github.com/sam-fakhreddine/str2bool3', + long_description=long_description, + long_description_content_type='text/markdown', download_url=f'https://github.com/sam-fakhreddine/str2bool3/archive/refs/tags/{version}.tar.gz', keywords=['str2bool', 'bool', 'boolean', 'convert', 'yes', 'no', 'true', 'false'], classifiers=[ From 8f995ff6c40845cf877fd391e51a557bcb545092 Mon Sep 17 00:00:00 2001 From: Sam Fakhreddine Date: Tue, 17 Oct 2023 15:08:47 -0600 Subject: [PATCH 15/27] Update setup.py --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 863ebe8..9e88387 100644 --- a/setup.py +++ b/setup.py @@ -12,7 +12,7 @@ version=version, description='Convert string to boolean (Forked from SymonSoft/str2bool)', author='Your Name', - author_email='your.email@example.com', + author_email='sam.fakhreddine@gmail.com', url='https://github.com/sam-fakhreddine/str2bool3', long_description=long_description, long_description_content_type='text/markdown', From 3d3389b2ec72f12d5a4fbc2b89e2922307a2509c Mon Sep 17 00:00:00 2001 From: Sam Fakhreddine Date: Tue, 17 Oct 2023 15:46:59 -0600 Subject: [PATCH 16/27] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6561d2b..dcaca9d 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ Case insensitive. ## Examples Here's a basic example: - >>> from str2bool3 import str2bool3 + >>> from str2bool3 import str2bool >>> print(str2bool3('Yes')) True From 963fc48f2bedc45012edf61c71e21c8df8306d3b Mon Sep 17 00:00:00 2001 From: Sam Fakhreddine Date: Tue, 17 Oct 2023 15:47:53 -0600 Subject: [PATCH 17/27] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index dcaca9d..deb0925 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ Case insensitive. Here's a basic example: >>> from str2bool3 import str2bool - >>> print(str2bool3('Yes')) + >>> print(str2bool('Yes')) True ## License From 297b40133a5720349fe057e349c9efb28b55cd47 Mon Sep 17 00:00:00 2001 From: Sam Fakhreddine Date: Tue, 17 Oct 2023 16:08:11 -0600 Subject: [PATCH 18/27] fixed some typos --- .github/workflows/workflows.yml | 3 --- setup.py | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/workflows.yml b/.github/workflows/workflows.yml index 80271a2..f6d5a4b 100644 --- a/.github/workflows/workflows.yml +++ b/.github/workflows/workflows.yml @@ -15,11 +15,8 @@ jobs: uses: actions/setup-python@v2 with: python-version: '3.x' - - name: Extract version from git tag run: echo "PACKAGE_VERSION=${{ github.ref_name }}" >> $GITHUB_ENV - - - name: Install pypa/build run: python3 -m pip install build --user - name: Build a binary wheel and a source tarball diff --git a/setup.py b/setup.py index 9e88387..573f603 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ packages=['str2bool3'], version=version, description='Convert string to boolean (Forked from SymonSoft/str2bool)', - author='Your Name', + author='Sam Fakhreddine', author_email='sam.fakhreddine@gmail.com', url='https://github.com/sam-fakhreddine/str2bool3', long_description=long_description, From 1e03f3442b13ddf7f91cb139440554323a17cd1a Mon Sep 17 00:00:00 2001 From: Sam Fakhreddine Date: Thu, 29 Aug 2024 18:47:40 -0600 Subject: [PATCH 19/27] Update __init__.py --- str2bool3/__init__.py | 36 ++++++++++++++++++++---------------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/str2bool3/__init__.py b/str2bool3/__init__.py index 4b90f67..0897d21 100644 --- a/str2bool3/__init__.py +++ b/str2bool3/__init__.py @@ -1,22 +1,26 @@ -from typing import Union +from typing import Optional -# Refactored Python 3.9 compatible code for string to boolean conversion +class StrUtils: + TRUE_SET = {'yes', 'true', 't', 'y', '1'} + FALSE_SET = {'no', 'false', 'f', 'n', '0'} + + @staticmethod + def str2bool(value: Optional[str], raise_exc: bool = False, default: Optional[bool] = None) -> Optional[bool]: + if value is None: + return default -def str2bool(value: Union[str, None], raise_exc: bool = False) -> Union[bool, None]: - true_set = {'yes', 'true', 't', 'y', '1'} - false_set = {'no', 'false', 'f', 'n', '0'} - - if isinstance(value, str): value = value.lower() - if value in true_set: + if value in StrUtils.TRUE_SET: return True - if value in false_set: + if value in StrUtils.FALSE_SET: return False - - if raise_exc: - raise ValueError(f'Expected one of: {", ".join(true_set | false_set)}') - - return None -def str2bool_exc(value: Union[str, None]) -> bool: - return str2bool(value, raise_exc=True) + if raise_exc: + valid_values = ', '.join(sorted(StrUtils.TRUE_SET | StrUtils.FALSE_SET)) + raise ValueError(f"Invalid value '{value}'. Expected one of: {valid_values}.") + + return default + + @staticmethod + def str2bool_exc(value: Optional[str]) -> bool: + return StrUtils.str2bool(value, raise_exc=True) From e2d5425291a773d0cdeeac8d1163c92407b5ac78 Mon Sep 17 00:00:00 2001 From: Sam Fakhreddine Date: Thu, 29 Aug 2024 18:48:34 -0600 Subject: [PATCH 20/27] Update README.md --- README.md | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index deb0925..6b1433a 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # str2bool3 v1.0.0 ## About -Convert string to boolean. -Library recognizes "yes", "true", "y", "t", "1" as True, and "no", "false", "n", "f", "0" as False. +Convert string to boolean. +The library recognizes "yes", "true", "y", "t", "1" as True, and "no", "false", "n", "f", "0" as False. Case insensitive. ## Installation @@ -12,11 +12,17 @@ Case insensitive. ## Examples Here's a basic example: - >>> from str2bool3 import str2bool - >>> print(str2bool('Yes')) + >>> from str2bool3 import StrUtils + >>> print(StrUtils.str2bool('Yes')) True +To raise an exception on invalid input: + + >>> from str2bool3 import StrUtils + >>> StrUtils.str2bool_exc('invalid') + ValueError: Invalid value 'invalid'. Expected one of: false, f, n, no, t, true, y, yes, 0, 1. + ## License BSD -forked from symonsoft/str2bool +Forked from symonsoft/str2bool From 7034208b9d3543c94204c1d50f270586bb165a95 Mon Sep 17 00:00:00 2001 From: Sam Fakhreddine Date: Thu, 29 Aug 2024 18:50:54 -0600 Subject: [PATCH 21/27] Update __init__.py --- str2bool3/__init__.py | 27 +-------------------------- 1 file changed, 1 insertion(+), 26 deletions(-) diff --git a/str2bool3/__init__.py b/str2bool3/__init__.py index 0897d21..1b01fea 100644 --- a/str2bool3/__init__.py +++ b/str2bool3/__init__.py @@ -1,26 +1 @@ -from typing import Optional - -class StrUtils: - TRUE_SET = {'yes', 'true', 't', 'y', '1'} - FALSE_SET = {'no', 'false', 'f', 'n', '0'} - - @staticmethod - def str2bool(value: Optional[str], raise_exc: bool = False, default: Optional[bool] = None) -> Optional[bool]: - if value is None: - return default - - value = value.lower() - if value in StrUtils.TRUE_SET: - return True - if value in StrUtils.FALSE_SET: - return False - - if raise_exc: - valid_values = ', '.join(sorted(StrUtils.TRUE_SET | StrUtils.FALSE_SET)) - raise ValueError(f"Invalid value '{value}'. Expected one of: {valid_values}.") - - return default - - @staticmethod - def str2bool_exc(value: Optional[str]) -> bool: - return StrUtils.str2bool(value, raise_exc=True) +from .str_utils import str2bool, str2bool_exc From a043350545aa2c1c3a78f8098f7d358cd869190a Mon Sep 17 00:00:00 2001 From: Sam Fakhreddine Date: Thu, 29 Aug 2024 18:51:47 -0600 Subject: [PATCH 22/27] Create str_utils.py --- str2bool3/str_utils.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 str2bool3/str_utils.py diff --git a/str2bool3/str_utils.py b/str2bool3/str_utils.py new file mode 100644 index 0000000..5c5a769 --- /dev/null +++ b/str2bool3/str_utils.py @@ -0,0 +1,23 @@ +from typing import Optional + +TRUE_SET = {'yes', 'true', 't', 'y', '1'} +FALSE_SET = {'no', 'false', 'f', 'n', '0'} + +def str2bool(value: Optional[str], raise_exc: bool = False, default: Optional[bool] = None) -> Optional[bool]: + if value is None: + return default + + value = value.lower() + if value in TRUE_SET: + return True + if value in FALSE_SET: + return False + + if raise_exc: + valid_values = ', '.join(sorted(TRUE_SET | FALSE_SET)) + raise ValueError(f"Invalid value '{value}'. Expected one of: {valid_values}.") + + return default + +def str2bool_exc(value: Optional[str]) -> bool: + return str2bool(value, raise_exc=True) From 0ad4bb50ae798c46cdba6cf6aa17d2769e925c41 Mon Sep 17 00:00:00 2001 From: Sam Fakhreddine Date: Thu, 29 Aug 2024 19:06:31 -0600 Subject: [PATCH 23/27] Update setup.py --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 573f603..2a88dde 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup # Read version from environment variable, with fallback to default version -version = os.environ.get('PACKAGE_VERSION', '1.0.0') +version = os.environ.get('PACKAGE_VERSION', '1.3.0') with open("README.md", "r", encoding="utf-8") as f: long_description = f.read() From ec3f2c08045eb8e7085a9fa67c7bb234c89ef008 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 13 Apr 2026 12:47:56 +0000 Subject: [PATCH 24/27] Release v1.4.0: improve robustness, add test suite, modernize packaging MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Robustness fixes in str_utils.py: - Strip leading/trailing whitespace from string inputs - Accept bool inputs directly (pass-through True/False) - Accept int inputs: 1β†’True, 0β†’False; other ints are invalid - Raise TypeError for unsupported types (float, list, etc.) - Fix str2bool_exc(None): now raises ValueError instead of silently returning None, consistent with the function's contract Testing: - Add tests/test_str_utils.py with 76 tests covering all inputs, edge cases, error messages, and both str2bool / str2bool_exc Packaging: - Add pyproject.toml (PEP 517/518); keep setup.py for CI version injection from git tags - Update README: correct import examples, document new behaviour, add API reference table CI/CD: - Add matrix test job across Python 3.8–3.12 before build/publish - Gate build on tests passing; gate publish on build - Bump actions/checkout, setup-python, upload/download-artifact to latest versions (v4/v5) - Trigger tests on push to master and all pull requests, not just tags https://claude.ai/code/session_017rKszX11QmRRdhMhBf9vCu --- .github/workflows/workflows.yml | 87 ++++++++----- README.md | 71 ++++++++-- pyproject.toml | 37 ++++++ setup.py | 41 ++---- str2bool3/str_utils.py | 83 ++++++++++-- tests/__init__.py | 0 tests/test_str_utils.py | 221 ++++++++++++++++++++++++++++++++ 7 files changed, 455 insertions(+), 85 deletions(-) create mode 100644 pyproject.toml create mode 100644 tests/__init__.py create mode 100644 tests/test_str_utils.py diff --git a/.github/workflows/workflows.yml b/.github/workflows/workflows.yml index f6d5a4b..c006261 100644 --- a/.github/workflows/workflows.yml +++ b/.github/workflows/workflows.yml @@ -1,37 +1,59 @@ -name: Publish Python 🐍 distribution πŸ“¦ to PyPI and TestPyPI +name: Test, Build, and Publish πŸ“¦ -on: +on: push: - tags: - - '*' + branches: ["master"] + tags: ["*"] + pull_request: + branches: ["master"] jobs: + test: + name: Run tests πŸ§ͺ + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] + steps: + - uses: actions/checkout@v4 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: pip install pytest + - name: Install package + run: pip install -e . + - name: Run tests + run: pytest tests/ -v + build: name: Build distribution πŸ“¦ runs-on: ubuntu-latest + needs: [test] steps: - - uses: actions/checkout@v3 - - name: Set up Python - uses: actions/setup-python@v2 - with: - python-version: '3.x' - - name: Extract version from git tag - run: echo "PACKAGE_VERSION=${{ github.ref_name }}" >> $GITHUB_ENV - - name: Install pypa/build - run: python3 -m pip install build --user - - name: Build a binary wheel and a source tarball - run: python3 -m build - - name: Store the distribution packages - uses: actions/upload-artifact@v2 - with: - name: python-package-distributions - path: dist/ + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.x" + - name: Extract version from git tag + if: startsWith(github.ref, 'refs/tags/') + run: echo "PACKAGE_VERSION=${{ github.ref_name }}" >> $GITHUB_ENV + - name: Install pypa/build + run: python3 -m pip install build --user + - name: Build a binary wheel and a source tarball + run: python3 -m build + - name: Store the distribution packages + uses: actions/upload-artifact@v4 + with: + name: python-package-distributions + path: dist/ publish-to-pypi: name: Publish Python 🐍 distribution πŸ“¦ to PyPI if: startsWith(github.ref, 'refs/tags/') - needs: - - build + needs: [build] runs-on: ubuntu-latest environment: name: pypi @@ -40,14 +62,13 @@ jobs: id-token: write steps: - - name: Download all the dists - uses: actions/download-artifact@v2 - with: - name: python-package-distributions - path: dist/ - - name: Publish distribution πŸ“¦ to PyPI - uses: pypa/gh-action-pypi-publish@release/v1 - with: - user: __token__ - password: ${{ secrets.PYPI_API_TOKEN }} - + - name: Download all the dists + uses: actions/download-artifact@v4 + with: + name: python-package-distributions + path: dist/ + - name: Publish distribution πŸ“¦ to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + user: __token__ + password: ${{ secrets.PYPI_API_TOKEN }} diff --git a/README.md b/README.md index 6b1433a..87c7c4e 100644 --- a/README.md +++ b/README.md @@ -1,28 +1,75 @@ -# str2bool3 v1.0.0 +# str2bool3 v1.4.0 ## About -Convert string to boolean. -The library recognizes "yes", "true", "y", "t", "1" as True, and "no", "false", "n", "f", "0" as False. -Case insensitive. +Convert a string (or compatible type) to a boolean value. + +Recognized **True** values: `yes`, `true`, `t`, `y`, `1` +Recognized **False** values: `no`, `false`, `f`, `n`, `0` + +Matching is case-insensitive and leading/trailing whitespace is stripped automatically. + +`bool` and `int` inputs are also accepted directly: +- `True` / `False` β†’ returned as-is +- `1` β†’ `True`, `0` β†’ `False` ## Installation $ pip install str2bool3 ## Examples -Here's a basic example: - >>> from str2bool3 import StrUtils - >>> print(StrUtils.str2bool('Yes')) +Basic usage: + + >>> from str2bool3 import str2bool + >>> str2bool('Yes') + True + >>> str2bool('no') + False + >>> str2bool(' TRUE ') # whitespace stripped True + >>> str2bool(True) # bool pass-through + True + >>> str2bool(1) # int support + True + +Unrecognized values return `None` by default (or a custom default): + + >>> str2bool('maybe') + None + >>> str2bool('maybe', default=False) + False + +Raise an exception for invalid input: + + >>> str2bool('maybe', raise_exc=True) + ValueError: Invalid value 'maybe'. Expected one of: 0, 1, f, false, n, no, t, true, y, yes. + +Convenience wrapper that always raises on invalid input (including `None`): + + >>> from str2bool3 import str2bool_exc + >>> str2bool_exc('invalid') + ValueError: Invalid value 'invalid'. Expected one of: 0, 1, f, false, n, no, t, true, y, yes. + >>> str2bool_exc(None) + ValueError: Cannot convert None to bool. ... + +## API + +### `str2bool(value, raise_exc=False, default=None)` + +| Parameter | Type | Description | +|-----------|------|-------------| +| `value` | `str \| bool \| int \| None` | Value to convert | +| `raise_exc` | `bool` | Raise `ValueError` on unrecognized input (default `False`) | +| `default` | `bool \| None` | Return value when input is unrecognized (default `None`) | + +Raises `TypeError` for unsupported types (e.g. `float`, `list`). -To raise an exception on invalid input: +### `str2bool_exc(value)` - >>> from str2bool3 import StrUtils - >>> StrUtils.str2bool_exc('invalid') - ValueError: Invalid value 'invalid'. Expected one of: false, f, n, no, t, true, y, yes, 0, 1. +Shorthand for `str2bool(value, raise_exc=True)`. Always raises `ValueError` +for `None` or unrecognized values. ## License BSD -Forked from symonsoft/str2bool +Forked from [symonsoft/str2bool](https://github.com/symonsoft/str2bool) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..1dbc38e --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,37 @@ +[build-system] +requires = ["setuptools>=61", "wheel"] +build-backend = "setuptools.backends.legacy:build" + +[project] +name = "str2bool3" +version = "1.4.0" +description = "Convert string to boolean (Forked from SymonSoft/str2bool)" +readme = {file = "README.md", content-type = "text/markdown"} +license = {text = "BSD-3-Clause"} +authors = [ + {name = "Sam Fakhreddine", email = "sam.fakhreddine@gmail.com"} +] +keywords = ["str2bool", "bool", "boolean", "convert", "yes", "no", "true", "false"] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "License :: OSI Approved :: BSD License", + "Operating System :: OS Independent", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Topic :: Utilities", +] +requires-python = ">=3.8" + +[project.urls] +Homepage = "https://github.com/sam-fakhreddine/str2bool3" +Repository = "https://github.com/sam-fakhreddine/str2bool3" + +[tool.setuptools.packages.find] +where = ["."] +include = ["str2bool3*"] + +[tool.pytest.ini_options] +testpaths = ["tests"] diff --git a/setup.py b/setup.py index 2a88dde..c550f9f 100644 --- a/setup.py +++ b/setup.py @@ -1,32 +1,9 @@ -import os -from setuptools import setup - -# Read version from environment variable, with fallback to default version -version = os.environ.get('PACKAGE_VERSION', '1.3.0') -with open("README.md", "r", encoding="utf-8") as f: - long_description = f.read() - -setup( - name='str2bool3', - packages=['str2bool3'], - version=version, - description='Convert string to boolean (Forked from SymonSoft/str2bool)', - author='Sam Fakhreddine', - author_email='sam.fakhreddine@gmail.com', - url='https://github.com/sam-fakhreddine/str2bool3', - long_description=long_description, - long_description_content_type='text/markdown', - download_url=f'https://github.com/sam-fakhreddine/str2bool3/archive/refs/tags/{version}.tar.gz', - keywords=['str2bool', 'bool', 'boolean', 'convert', 'yes', 'no', 'true', 'false'], - classifiers=[ - 'Development Status :: 5 - Production/Stable', - 'License :: OSI Approved :: BSD License', - 'Operating System :: OS Independent', - 'Programming Language :: Python :: 3.8', - 'Programming Language :: Python :: 3.9', - 'Programming Language :: Python :: 3.10', - 'Programming Language :: Python :: 3.11', - 'Programming Language :: Python :: 3.12', - 'Topic :: Utilities' - ], -) +import os +from setuptools import setup + +# Allow CI to override version via PACKAGE_VERSION env var (set from git tag) +version = os.environ.get('PACKAGE_VERSION') +if version: + setup(version=version) +else: + setup() diff --git a/str2bool3/str_utils.py b/str2bool3/str_utils.py index 5c5a769..e5b49b1 100644 --- a/str2bool3/str_utils.py +++ b/str2bool3/str_utils.py @@ -1,23 +1,90 @@ -from typing import Optional +from typing import Optional, Union TRUE_SET = {'yes', 'true', 't', 'y', '1'} FALSE_SET = {'no', 'false', 'f', 'n', '0'} -def str2bool(value: Optional[str], raise_exc: bool = False, default: Optional[bool] = None) -> Optional[bool]: + +def str2bool( + value: Optional[Union[str, bool, int]], + raise_exc: bool = False, + default: Optional[bool] = None, +) -> Optional[bool]: + """Convert a string (or compatible type) to a boolean. + + Recognizes 'yes', 'true', 't', 'y', '1' as True and + 'no', 'false', 'f', 'n', '0' as False. Case-insensitive. + Leading/trailing whitespace is stripped automatically. + + bool and int inputs are handled directly: + - bool: returned as-is (True/False) + - int 1: True, int 0: False, other ints: unrecognized + + Args: + value: The value to convert. Strings, bools, ints, and None accepted. + raise_exc: If True, raise ValueError for unrecognized values + (including None). + default: Value to return when input is unrecognized and + raise_exc is False. + + Returns: + True, False, or default. + + Raises: + TypeError: If value is not str, bool, int, or None. + ValueError: If value is unrecognized and raise_exc is True. + """ + # bool must be checked before int (bool is a subclass of int) + if isinstance(value, bool): + return value + + if isinstance(value, int): + if value == 1: + return True + if value == 0: + return False + if raise_exc: + raise ValueError( + f"Invalid integer value '{value}'. Expected 0 or 1." + ) + return default + if value is None: + if raise_exc: + raise ValueError( + "Cannot convert None to bool. Expected one of: " + + ', '.join(sorted(TRUE_SET | FALSE_SET)) + '.' + ) return default - - value = value.lower() + + if not isinstance(value, str): + raise TypeError( + f"Expected str, bool, int, or None; got {type(value).__name__!r}." + ) + + value = value.strip().lower() + if value in TRUE_SET: return True if value in FALSE_SET: return False - + if raise_exc: valid_values = ', '.join(sorted(TRUE_SET | FALSE_SET)) - raise ValueError(f"Invalid value '{value}'. Expected one of: {valid_values}.") - + raise ValueError( + f"Invalid value '{value}'. Expected one of: {valid_values}." + ) + return default -def str2bool_exc(value: Optional[str]) -> bool: + +def str2bool_exc(value: Optional[Union[str, bool, int]]) -> bool: + """Convert a string to boolean, raising ValueError for any invalid input. + + Equivalent to str2bool(value, raise_exc=True). None and unrecognized + strings both raise ValueError. + + Raises: + TypeError: If value is not str, bool, int, or None. + ValueError: If value is None or not a recognized boolean string. + """ return str2bool(value, raise_exc=True) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_str_utils.py b/tests/test_str_utils.py new file mode 100644 index 0000000..968db8a --- /dev/null +++ b/tests/test_str_utils.py @@ -0,0 +1,221 @@ +import pytest +from str2bool3 import str2bool, str2bool_exc + + +# --------------------------------------------------------------------------- +# str2bool β€” true values +# --------------------------------------------------------------------------- + +class TestTrueValues: + @pytest.mark.parametrize("value", [ + "yes", "YES", "Yes", "yEs", + "true", "True", "TRUE", "tRuE", + "t", "T", + "y", "Y", + "1", + ]) + def test_string_true(self, value): + assert str2bool(value) is True + + def test_bool_true_passthrough(self): + assert str2bool(True) is True + + def test_int_one(self): + assert str2bool(1) is True + + +# --------------------------------------------------------------------------- +# str2bool β€” false values +# --------------------------------------------------------------------------- + +class TestFalseValues: + @pytest.mark.parametrize("value", [ + "no", "NO", "No", "nO", + "false", "False", "FALSE", "fAlSe", + "f", "F", + "n", "N", + "0", + ]) + def test_string_false(self, value): + assert str2bool(value) is False + + def test_bool_false_passthrough(self): + assert str2bool(False) is False + + def test_int_zero(self): + assert str2bool(0) is False + + +# --------------------------------------------------------------------------- +# str2bool β€” whitespace handling +# --------------------------------------------------------------------------- + +class TestWhitespace: + def test_leading_space_true(self): + assert str2bool(" yes") is True + + def test_trailing_space_true(self): + assert str2bool("yes ") is True + + def test_both_spaces_true(self): + assert str2bool(" yes ") is True + + def test_tab_whitespace_true(self): + assert str2bool("\tyes\t") is True + + def test_leading_space_false(self): + assert str2bool(" no") is False + + def test_trailing_space_false(self): + assert str2bool("no ") is False + + def test_both_spaces_false(self): + assert str2bool(" no ") is False + + +# --------------------------------------------------------------------------- +# str2bool β€” None and defaults +# --------------------------------------------------------------------------- + +class TestNoneAndDefaults: + def test_none_returns_none_by_default(self): + assert str2bool(None) is None + + def test_none_with_custom_default_true(self): + assert str2bool(None, default=True) is True + + def test_none_with_custom_default_false(self): + assert str2bool(None, default=False) is False + + def test_invalid_string_returns_none_by_default(self): + assert str2bool("maybe") is None + + def test_invalid_string_with_custom_default(self): + assert str2bool("maybe", default=False) is False + + def test_empty_string_returns_none(self): + assert str2bool("") is None + + def test_empty_string_with_default(self): + assert str2bool("", default=True) is True + + def test_invalid_int_returns_none(self): + assert str2bool(2) is None + + def test_invalid_int_with_default(self): + assert str2bool(2, default=False) is False + + +# --------------------------------------------------------------------------- +# str2bool β€” raise_exc=True +# --------------------------------------------------------------------------- + +class TestRaiseExc: + def test_valid_true_no_exception(self): + assert str2bool("yes", raise_exc=True) is True + + def test_valid_false_no_exception(self): + assert str2bool("no", raise_exc=True) is False + + def test_bool_true_no_exception(self): + assert str2bool(True, raise_exc=True) is True + + def test_bool_false_no_exception(self): + assert str2bool(False, raise_exc=True) is False + + def test_int_one_no_exception(self): + assert str2bool(1, raise_exc=True) is True + + def test_int_zero_no_exception(self): + assert str2bool(0, raise_exc=True) is False + + def test_none_raises_value_error(self): + with pytest.raises(ValueError): + str2bool(None, raise_exc=True) + + def test_invalid_string_raises_value_error(self): + with pytest.raises(ValueError): + str2bool("maybe", raise_exc=True) + + def test_empty_string_raises_value_error(self): + with pytest.raises(ValueError): + str2bool("", raise_exc=True) + + def test_invalid_int_raises_value_error(self): + with pytest.raises(ValueError): + str2bool(2, raise_exc=True) + + def test_error_message_contains_invalid_value(self): + with pytest.raises(ValueError, match="maybe"): + str2bool("maybe", raise_exc=True) + + def test_error_message_mentions_expected_values(self): + with pytest.raises(ValueError, match="Expected one of"): + str2bool("nope", raise_exc=True) + + +# --------------------------------------------------------------------------- +# str2bool β€” TypeError for unsupported types +# --------------------------------------------------------------------------- + +class TestTypeError: + @pytest.mark.parametrize("value", [ + 1.0, + [], + {}, + object(), + b"yes", + ]) + def test_unsupported_type_raises_type_error(self, value): + with pytest.raises(TypeError): + str2bool(value) + + def test_type_error_message_includes_type_name(self): + with pytest.raises(TypeError, match="float"): + str2bool(1.0) + + +# --------------------------------------------------------------------------- +# str2bool_exc +# --------------------------------------------------------------------------- + +class TestStr2BoolExc: + def test_valid_true(self): + assert str2bool_exc("yes") is True + + def test_valid_false(self): + assert str2bool_exc("no") is False + + def test_bool_true_passthrough(self): + assert str2bool_exc(True) is True + + def test_bool_false_passthrough(self): + assert str2bool_exc(False) is False + + def test_int_one(self): + assert str2bool_exc(1) is True + + def test_int_zero(self): + assert str2bool_exc(0) is False + + def test_none_raises(self): + with pytest.raises(ValueError): + str2bool_exc(None) + + def test_invalid_string_raises(self): + with pytest.raises(ValueError): + str2bool_exc("invalid") + + def test_invalid_int_raises(self): + with pytest.raises(ValueError): + str2bool_exc(2) + + def test_whitespace_true(self): + assert str2bool_exc(" yes ") is True + + def test_whitespace_false(self): + assert str2bool_exc(" no ") is False + + def test_unsupported_type_raises_type_error(self): + with pytest.raises(TypeError): + str2bool_exc(1.0) From d822b0414492c2396ddd0dd2e96027c4ac6a0f4d Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 13 Apr 2026 12:56:39 +0000 Subject: [PATCH 25/27] Fix pyproject.toml build backend causing CI install failure Replaced non-existent 'setuptools.backends.legacy:build' with the correct 'setuptools.build_meta' backend. Also moved version to dynamic (provided by setup.py) to avoid duplicate-version conflict between pyproject.toml and setup.py. https://claude.ai/code/session_017rKszX11QmRRdhMhBf9vCu --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 1dbc38e..e8600d9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,10 +1,10 @@ [build-system] requires = ["setuptools>=61", "wheel"] -build-backend = "setuptools.backends.legacy:build" +build-backend = "setuptools.build_meta" [project] name = "str2bool3" -version = "1.4.0" +dynamic = ["version"] description = "Convert string to boolean (Forked from SymonSoft/str2bool)" readme = {file = "README.md", content-type = "text/markdown"} license = {text = "BSD-3-Clause"} From 04b80957c10e9a7922ed4300b240409ca855ccb0 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 13 Apr 2026 13:02:12 +0000 Subject: [PATCH 26/27] Add on/off/enabled/disabled; refactor to DRY _parse() core MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Value sets: - Add 'on'/'off' and 'enabled'/'disabled' to TRUE_VALUES/FALSE_VALUES - Rename TRUE_SET/FALSE_SET β†’ TRUE_VALUES/FALSE_VALUES (public, frozenset) - Export TRUE_VALUES and FALSE_VALUES via __init__.__all__ Refactor: - Extract _parse() β€” single place for all type-dispatch logic; both str2bool() and any future callers share it without duplication - Pre-compute _VALID string once at import time (was rebuilt on every raise_exc call) - _MISSING sentinel object cleanly separates 'unrecognized' from False/None return values - _Input type alias removes the repeated Optional[Union[str,bool,int]] Tests: 105 tests (up from 76), covering new values, public constants, frozenset immutability, and disjoint-set invariant https://claude.ai/code/session_017rKszX11QmRRdhMhBf9vCu --- str2bool3/__init__.py | 4 +- str2bool3/str_utils.py | 106 ++++++++++++++++++++-------------------- tests/test_str_utils.py | 65 +++++++++++++++++++++++- 3 files changed, 121 insertions(+), 54 deletions(-) diff --git a/str2bool3/__init__.py b/str2bool3/__init__.py index 1b01fea..b390a07 100644 --- a/str2bool3/__init__.py +++ b/str2bool3/__init__.py @@ -1 +1,3 @@ -from .str_utils import str2bool, str2bool_exc +from .str_utils import FALSE_VALUES, TRUE_VALUES, str2bool, str2bool_exc + +__all__ = ["str2bool", "str2bool_exc", "TRUE_VALUES", "FALSE_VALUES"] diff --git a/str2bool3/str_utils.py b/str2bool3/str_utils.py index e5b49b1..25d470d 100644 --- a/str2bool3/str_utils.py +++ b/str2bool3/str_utils.py @@ -1,39 +1,20 @@ from typing import Optional, Union -TRUE_SET = {'yes', 'true', 't', 'y', '1'} -FALSE_SET = {'no', 'false', 'f', 'n', '0'} +TRUE_VALUES: frozenset = frozenset({'yes', 'true', 't', 'y', '1', 'on', 'enabled'}) +FALSE_VALUES: frozenset = frozenset({'no', 'false', 'f', 'n', '0', 'off', 'disabled'}) +_VALID: str = ', '.join(sorted(TRUE_VALUES | FALSE_VALUES)) +_MISSING = object() # sentinel: value was not recognized -def str2bool( - value: Optional[Union[str, bool, int]], - raise_exc: bool = False, - default: Optional[bool] = None, -) -> Optional[bool]: - """Convert a string (or compatible type) to a boolean. +_Input = Optional[Union[str, bool, int]] - Recognizes 'yes', 'true', 't', 'y', '1' as True and - 'no', 'false', 'f', 'n', '0' as False. Case-insensitive. - Leading/trailing whitespace is stripped automatically. - bool and int inputs are handled directly: - - bool: returned as-is (True/False) - - int 1: True, int 0: False, other ints: unrecognized - - Args: - value: The value to convert. Strings, bools, ints, and None accepted. - raise_exc: If True, raise ValueError for unrecognized values - (including None). - default: Value to return when input is unrecognized and - raise_exc is False. - - Returns: - True, False, or default. +def _parse(value: _Input) -> object: + """Coerce *value* to True, False, or _MISSING. - Raises: - TypeError: If value is not str, bool, int, or None. - ValueError: If value is unrecognized and raise_exc is True. + Raises TypeError for unsupported types so callers never have to. """ - # bool must be checked before int (bool is a subclass of int) + # bool must come before int β€” bool is a subclass of int if isinstance(value, bool): return value @@ -42,49 +23,70 @@ def str2bool( return True if value == 0: return False - if raise_exc: - raise ValueError( - f"Invalid integer value '{value}'. Expected 0 or 1." - ) - return default + return _MISSING if value is None: - if raise_exc: - raise ValueError( - "Cannot convert None to bool. Expected one of: " - + ', '.join(sorted(TRUE_SET | FALSE_SET)) + '.' - ) - return default + return _MISSING if not isinstance(value, str): raise TypeError( f"Expected str, bool, int, or None; got {type(value).__name__!r}." ) - value = value.strip().lower() - - if value in TRUE_SET: + normalized = value.strip().lower() + if normalized in TRUE_VALUES: return True - if value in FALSE_SET: + if normalized in FALSE_VALUES: return False + return _MISSING + + +def str2bool( + value: _Input, + raise_exc: bool = False, + default: Optional[bool] = None, +) -> Optional[bool]: + """Convert a value to bool. + + Recognized True: yes, true, t, y, 1, on, enabled + Recognized False: no, false, f, n, 0, off, disabled + + Matching is case-insensitive; leading/trailing whitespace is stripped. + bool inputs are passed through; int 1/0 map to True/False. + + Args: + value: Input to convert. Accepts str, bool, int, or None. + raise_exc: Raise ValueError for unrecognized values (including None). + default: Returned when value is unrecognized and raise_exc is False. + + Returns: + True, False, or default. + + Raises: + TypeError: If value is not str, bool, int, or None. + ValueError: If value is unrecognized and raise_exc is True. + """ + result = _parse(value) + if result is not _MISSING: + return result # type: ignore[return-value] if raise_exc: - valid_values = ', '.join(sorted(TRUE_SET | FALSE_SET)) - raise ValueError( - f"Invalid value '{value}'. Expected one of: {valid_values}." - ) + if value is None: + raise ValueError(f"Cannot convert None to bool. Expected one of: {_VALID}.") + if isinstance(value, int): + raise ValueError(f"Invalid integer {value!r}. Expected 0 or 1.") + raise ValueError(f"Invalid value {value!r}. Expected one of: {_VALID}.") return default -def str2bool_exc(value: Optional[Union[str, bool, int]]) -> bool: - """Convert a string to boolean, raising ValueError for any invalid input. +def str2bool_exc(value: _Input) -> bool: + """Convert to bool, raising ValueError on any invalid input (including None). - Equivalent to str2bool(value, raise_exc=True). None and unrecognized - strings both raise ValueError. + Shorthand for ``str2bool(value, raise_exc=True)``. Raises: TypeError: If value is not str, bool, int, or None. - ValueError: If value is None or not a recognized boolean string. + ValueError: If value is None or unrecognized. """ return str2bool(value, raise_exc=True) diff --git a/tests/test_str_utils.py b/tests/test_str_utils.py index 968db8a..5453f9a 100644 --- a/tests/test_str_utils.py +++ b/tests/test_str_utils.py @@ -1,5 +1,26 @@ import pytest -from str2bool3 import str2bool, str2bool_exc +from str2bool3 import FALSE_VALUES, TRUE_VALUES, str2bool, str2bool_exc + + +# --------------------------------------------------------------------------- +# Public constants +# --------------------------------------------------------------------------- + +class TestValueSets: + def test_true_values_is_frozenset(self): + assert isinstance(TRUE_VALUES, frozenset) + + def test_false_values_is_frozenset(self): + assert isinstance(FALSE_VALUES, frozenset) + + def test_true_and_false_sets_are_disjoint(self): + assert TRUE_VALUES.isdisjoint(FALSE_VALUES) + + def test_true_values_contains_expected(self): + assert {'yes', 'true', 't', 'y', '1', 'on', 'enabled'} <= TRUE_VALUES + + def test_false_values_contains_expected(self): + assert {'no', 'false', 'f', 'n', '0', 'off', 'disabled'} <= FALSE_VALUES # --------------------------------------------------------------------------- @@ -13,6 +34,8 @@ class TestTrueValues: "t", "T", "y", "Y", "1", + "on", "ON", "On", + "enabled", "ENABLED", "Enabled", ]) def test_string_true(self, value): assert str2bool(value) is True @@ -35,6 +58,8 @@ class TestFalseValues: "f", "F", "n", "N", "0", + "off", "OFF", "Off", + "disabled", "DISABLED", "Disabled", ]) def test_string_false(self, value): assert str2bool(value) is False @@ -72,6 +97,12 @@ def test_trailing_space_false(self): def test_both_spaces_false(self): assert str2bool(" no ") is False + def test_whitespace_around_on(self): + assert str2bool(" on ") is True + + def test_whitespace_around_disabled(self): + assert str2bool(" disabled ") is False + # --------------------------------------------------------------------------- # str2bool β€” None and defaults @@ -117,6 +148,12 @@ def test_valid_true_no_exception(self): def test_valid_false_no_exception(self): assert str2bool("no", raise_exc=True) is False + def test_on_no_exception(self): + assert str2bool("on", raise_exc=True) is True + + def test_disabled_no_exception(self): + assert str2bool("disabled", raise_exc=True) is False + def test_bool_true_no_exception(self): assert str2bool(True, raise_exc=True) is True @@ -153,6 +190,14 @@ def test_error_message_mentions_expected_values(self): with pytest.raises(ValueError, match="Expected one of"): str2bool("nope", raise_exc=True) + def test_none_error_mentions_expected_values(self): + with pytest.raises(ValueError, match="Expected one of"): + str2bool(None, raise_exc=True) + + def test_invalid_int_error_mentions_zero_one(self): + with pytest.raises(ValueError, match="0 or 1"): + str2bool(2, raise_exc=True) + # --------------------------------------------------------------------------- # str2bool β€” TypeError for unsupported types @@ -186,6 +231,18 @@ def test_valid_true(self): def test_valid_false(self): assert str2bool_exc("no") is False + def test_on(self): + assert str2bool_exc("on") is True + + def test_off(self): + assert str2bool_exc("off") is False + + def test_enabled(self): + assert str2bool_exc("enabled") is True + + def test_disabled(self): + assert str2bool_exc("disabled") is False + def test_bool_true_passthrough(self): assert str2bool_exc(True) is True @@ -216,6 +273,12 @@ def test_whitespace_true(self): def test_whitespace_false(self): assert str2bool_exc(" no ") is False + def test_whitespace_on(self): + assert str2bool_exc(" on ") is True + + def test_whitespace_disabled(self): + assert str2bool_exc(" disabled ") is False + def test_unsupported_type_raises_type_error(self): with pytest.raises(TypeError): str2bool_exc(1.0) From 279dac7621c1b80efa8150fd14c7103cbaada0f5 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 13 Apr 2026 13:05:34 +0000 Subject: [PATCH 27/27] Address review comments; add Python 3.13 and 3.14 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit setup.py / pyproject.toml β€” single version source of truth: - setup.py now always passes a version to setup() with a fallback default, eliminating the ambiguous no-version code path when PACKAGE_VERSION is unset (fixes the dual-declaration conflict noted in review) - Add Python 3.13 / 3.14 classifiers to pyproject.toml CI (workflows.yml): - Extend test matrix to include Python 3.13 and 3.14 Tests (109, up from 105): - test_none_raises_value_error: add match="Cannot convert None to bool" to lock in the user-facing error message - Add four whitespace-only input tests (' ', '\t\n') covering the default return, custom default, and raise_exc paths https://claude.ai/code/session_017rKszX11QmRRdhMhBf9vCu --- .github/workflows/workflows.yml | 2 +- pyproject.toml | 2 ++ setup.py | 9 +++------ tests/test_str_utils.py | 15 ++++++++++++++- 4 files changed, 20 insertions(+), 8 deletions(-) diff --git a/.github/workflows/workflows.yml b/.github/workflows/workflows.yml index c006261..5ff45cd 100644 --- a/.github/workflows/workflows.yml +++ b/.github/workflows/workflows.yml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} diff --git a/pyproject.toml b/pyproject.toml index e8600d9..4f7419e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,6 +21,8 @@ classifiers = [ "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", "Topic :: Utilities", ] requires-python = ">=3.8" diff --git a/setup.py b/setup.py index c550f9f..3a7888a 100644 --- a/setup.py +++ b/setup.py @@ -1,9 +1,6 @@ import os from setuptools import setup -# Allow CI to override version via PACKAGE_VERSION env var (set from git tag) -version = os.environ.get('PACKAGE_VERSION') -if version: - setup(version=version) -else: - setup() +# Single source of truth: PACKAGE_VERSION env var (set from git tag in CI), +# falling back to the default development version. +setup(version=os.environ.get('PACKAGE_VERSION', '1.4.0')) diff --git a/tests/test_str_utils.py b/tests/test_str_utils.py index 5453f9a..2a3afdd 100644 --- a/tests/test_str_utils.py +++ b/tests/test_str_utils.py @@ -130,6 +130,19 @@ def test_empty_string_returns_none(self): def test_empty_string_with_default(self): assert str2bool("", default=True) is True + def test_whitespace_only_returns_none(self): + assert str2bool(" ") is None + + def test_whitespace_only_tab_newline_returns_none(self): + assert str2bool("\t\n") is None + + def test_whitespace_only_with_default(self): + assert str2bool(" ", default=False) is False + + def test_whitespace_only_raises_when_raise_exc(self): + with pytest.raises(ValueError, match="Expected one of"): + str2bool(" ", raise_exc=True) + def test_invalid_int_returns_none(self): assert str2bool(2) is None @@ -167,7 +180,7 @@ def test_int_zero_no_exception(self): assert str2bool(0, raise_exc=True) is False def test_none_raises_value_error(self): - with pytest.raises(ValueError): + with pytest.raises(ValueError, match="Cannot convert None to bool"): str2bool(None, raise_exc=True) def test_invalid_string_raises_value_error(self):