textutils-hs 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,8 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Hamza Fikri
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction. See the full MIT License text at
8
+ https://opensource.org/license/mit.
@@ -0,0 +1,93 @@
1
+ Metadata-Version: 2.4
2
+ Name: textutils-hs
3
+ Version: 0.1.0
4
+ Summary: Small, dependency-free helpers for common text tasks.
5
+ Author: Hamza Fikri
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/hamzasinko/textutils
8
+ Project-URL: Repository, https://github.com/hamzasinko/textutils
9
+ Project-URL: Issues, https://github.com/hamzasinko/textutils/issues
10
+ Project-URL: Changelog, https://github.com/hamzasinko/textutils/blob/main/CHANGELOG.md
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3 :: Only
16
+ Classifier: Programming Language :: Python :: 3.9
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Topic :: Text Processing
21
+ Requires-Python: >=3.9
22
+ Description-Content-Type: text/markdown
23
+ License-File: LICENSE
24
+ Provides-Extra: dev
25
+ Requires-Dist: pytest>=8.0; extra == "dev"
26
+ Requires-Dist: pytest-cov>=5.0; extra == "dev"
27
+ Requires-Dist: black>=24.0; extra == "dev"
28
+ Requires-Dist: ruff>=0.5; extra == "dev"
29
+ Requires-Dist: pre-commit>=3.7; extra == "dev"
30
+ Requires-Dist: build>=1.2; extra == "dev"
31
+ Requires-Dist: twine>=5.0; extra == "dev"
32
+ Dynamic: license-file
33
+
34
+ # textutils
35
+
36
+ Small, dependency-free Python helpers for common text tasks: counting words,
37
+ counting characters, reversing a string, and capitalizing each word. Every
38
+ function has type hints, a full docstring, and tests.
39
+
40
+ ## Features
41
+
42
+ - `word_count(text)` — number of words in a string.
43
+ - `character_count(text)` — number of characters in a string.
44
+ - `reverse(text)` — the string reversed.
45
+ - `capitalize_words(text)` — first letter of each word capitalized.
46
+ - `slugify(text)` — a URL-friendly slug (lowercase, hyphen-separated).
47
+
48
+ Edge cases are handled: empty strings return empty results, and non-string
49
+ input raises `TypeError`.
50
+
51
+ ## Installation
52
+
53
+ Requires Python 3.9 or newer. No third-party runtime dependencies.
54
+
55
+ Clone and install in editable mode with the development tools:
56
+
57
+ ```bash
58
+ git clone https://github.com/hamzasinko/textutils.git
59
+ cd textutils
60
+ python -m pip install -e ".[dev]"
61
+ ```
62
+
63
+ Run the tests:
64
+
65
+ ```bash
66
+ python -m pytest
67
+ ```
68
+
69
+ ## Usage
70
+
71
+ ```python
72
+ from textutils import word_count, character_count, reverse, capitalize_words, slugify
73
+
74
+ word_count("hello world") # 2
75
+ character_count("hello") # 5
76
+ reverse("stressed") # "desserts"
77
+ capitalize_words("hello world") # "Hello World"
78
+ slugify("Hello, World!") # "hello-world"
79
+ ```
80
+
81
+ ## Contributing
82
+
83
+ Contributions are welcome.
84
+
85
+ 1. Fork the repository on GitHub.
86
+ 2. Create a branch: `git checkout -b feature/my-change`.
87
+ 3. Make your change, keeping commits small and focused.
88
+ 4. Run the tests: `python -m pytest`.
89
+ 5. Push your branch and open a pull request.
90
+
91
+ ## License
92
+
93
+ Released under the MIT License. See [LICENSE](LICENSE) for details.
@@ -0,0 +1,60 @@
1
+ # textutils
2
+
3
+ Small, dependency-free Python helpers for common text tasks: counting words,
4
+ counting characters, reversing a string, and capitalizing each word. Every
5
+ function has type hints, a full docstring, and tests.
6
+
7
+ ## Features
8
+
9
+ - `word_count(text)` — number of words in a string.
10
+ - `character_count(text)` — number of characters in a string.
11
+ - `reverse(text)` — the string reversed.
12
+ - `capitalize_words(text)` — first letter of each word capitalized.
13
+ - `slugify(text)` — a URL-friendly slug (lowercase, hyphen-separated).
14
+
15
+ Edge cases are handled: empty strings return empty results, and non-string
16
+ input raises `TypeError`.
17
+
18
+ ## Installation
19
+
20
+ Requires Python 3.9 or newer. No third-party runtime dependencies.
21
+
22
+ Clone and install in editable mode with the development tools:
23
+
24
+ ```bash
25
+ git clone https://github.com/hamzasinko/textutils.git
26
+ cd textutils
27
+ python -m pip install -e ".[dev]"
28
+ ```
29
+
30
+ Run the tests:
31
+
32
+ ```bash
33
+ python -m pytest
34
+ ```
35
+
36
+ ## Usage
37
+
38
+ ```python
39
+ from textutils import word_count, character_count, reverse, capitalize_words, slugify
40
+
41
+ word_count("hello world") # 2
42
+ character_count("hello") # 5
43
+ reverse("stressed") # "desserts"
44
+ capitalize_words("hello world") # "Hello World"
45
+ slugify("Hello, World!") # "hello-world"
46
+ ```
47
+
48
+ ## Contributing
49
+
50
+ Contributions are welcome.
51
+
52
+ 1. Fork the repository on GitHub.
53
+ 2. Create a branch: `git checkout -b feature/my-change`.
54
+ 3. Make your change, keeping commits small and focused.
55
+ 4. Run the tests: `python -m pytest`.
56
+ 5. Push your branch and open a pull request.
57
+
58
+ ## License
59
+
60
+ Released under the MIT License. See [LICENSE](LICENSE) for details.
@@ -0,0 +1,75 @@
1
+ [build-system]
2
+ requires = ["setuptools>=77.0.3"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "textutils-hs"
7
+ version = "0.1.0"
8
+ description = "Small, dependency-free helpers for common text tasks."
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ license-files = ["LICENSE"]
12
+ requires-python = ">=3.9"
13
+ authors = [
14
+ { name = "Hamza Fikri" },
15
+ ]
16
+ classifiers = [
17
+ "Development Status :: 3 - Alpha",
18
+ "Intended Audience :: Developers",
19
+ "Operating System :: OS Independent",
20
+ "Programming Language :: Python :: 3",
21
+ "Programming Language :: Python :: 3 :: Only",
22
+ "Programming Language :: Python :: 3.9",
23
+ "Programming Language :: Python :: 3.10",
24
+ "Programming Language :: Python :: 3.11",
25
+ "Programming Language :: Python :: 3.12",
26
+ "Topic :: Text Processing",
27
+ ]
28
+ dependencies = []
29
+
30
+ [project.optional-dependencies]
31
+ dev = [
32
+ "pytest>=8.0",
33
+ "pytest-cov>=5.0",
34
+ "black>=24.0",
35
+ "ruff>=0.5",
36
+ "pre-commit>=3.7",
37
+ "build>=1.2",
38
+ "twine>=5.0",
39
+ ]
40
+
41
+ [project.urls]
42
+ Homepage = "https://github.com/hamzasinko/textutils"
43
+ Repository = "https://github.com/hamzasinko/textutils"
44
+ Issues = "https://github.com/hamzasinko/textutils/issues"
45
+ Changelog = "https://github.com/hamzasinko/textutils/blob/main/CHANGELOG.md"
46
+
47
+ [tool.setuptools.packages.find]
48
+ include = ["textutils*"]
49
+
50
+ [tool.pytest.ini_options]
51
+ addopts = [
52
+ "--cov=textutils",
53
+ "--cov-report=term-missing",
54
+ ]
55
+ testpaths = ["tests"]
56
+
57
+ [tool.black]
58
+ line-length = 88
59
+ target-version = ["py39"]
60
+
61
+ [tool.ruff]
62
+ line-length = 88
63
+ target-version = "py39"
64
+
65
+ [tool.ruff.lint]
66
+ select = ["E", "F", "I", "N", "B", "UP", "D"]
67
+
68
+ [tool.ruff.lint.pydocstyle]
69
+ convention = "numpy"
70
+
71
+ [tool.coverage.run]
72
+ source = ["textutils"]
73
+
74
+ [tool.coverage.report]
75
+ show_missing = true
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,49 @@
1
+ """Tests for textutils.counting."""
2
+
3
+ import pytest
4
+
5
+ from textutils import character_count, word_count
6
+
7
+ # --- word_count -------------------------------------------------------------
8
+
9
+ WORD_COUNT_CASES = [
10
+ pytest.param("hello world", 2, id="counts-two-words"),
11
+ pytest.param("one two three", 3, id="counts-three-words"),
12
+ pytest.param("hello", 1, id="single-word"),
13
+ pytest.param("", 0, id="empty-string"),
14
+ pytest.param(" \t\n ", 0, id="whitespace-only"),
15
+ pytest.param(" a b c ", 3, id="ignores-multiple-spaces"),
16
+ pytest.param("a\tb\nc\r\nd", 4, id="tabs-and-newlines-separate-words"),
17
+ pytest.param(" hi ", 1, id="ignores-leading-trailing-whitespace"),
18
+ pytest.param("!!!", 1, id="punctuation-only-is-one-token"),
19
+ pytest.param("héllo wörld", 2, id="unicode-words"),
20
+ pytest.param("Hello WORLD", 2, id="mixed-case"),
21
+ pytest.param("word " * 10_000, 10_000, id="very-long-input"),
22
+ ]
23
+
24
+
25
+ @pytest.mark.parametrize("text, expected", WORD_COUNT_CASES)
26
+ def test_word_count(text: str, expected: int) -> None:
27
+ """Return the number of whitespace-separated words."""
28
+ assert word_count(text) == expected
29
+
30
+
31
+ # --- character_count --------------------------------------------------------
32
+
33
+ CHARACTER_COUNT_CASES = [
34
+ pytest.param("hello", 5, id="ascii"),
35
+ pytest.param("hi there", 8, id="counts-spaces"),
36
+ pytest.param("", 0, id="empty-string"),
37
+ pytest.param(" \t\n", 3, id="whitespace-only"),
38
+ pytest.param("a\tb\n", 4, id="counts-tabs-and-newlines"),
39
+ pytest.param("héllo", 5, id="unicode-code-points"),
40
+ pytest.param("👍", 1, id="emoji-single-code-point"),
41
+ pytest.param("e\u0301", 2, id="combining-mark-counts-twice"),
42
+ pytest.param("a" * 100_000, 100_000, id="very-long-input"),
43
+ ]
44
+
45
+
46
+ @pytest.mark.parametrize("text, expected", CHARACTER_COUNT_CASES)
47
+ def test_character_count(text: str, expected: int) -> None:
48
+ """Return the number of characters."""
49
+ assert character_count(text) == expected
@@ -0,0 +1,62 @@
1
+ """Tests for the public textutils API surface."""
2
+
3
+ import pytest
4
+
5
+ import textutils
6
+ from textutils import (
7
+ capitalize_words,
8
+ character_count,
9
+ reverse,
10
+ slugify,
11
+ word_count,
12
+ )
13
+
14
+ ALL_FUNCTIONS = (word_count, character_count, reverse, capitalize_words, slugify)
15
+
16
+ EXPECTED_ALL = [
17
+ "capitalize_words",
18
+ "character_count",
19
+ "reverse",
20
+ "slugify",
21
+ "word_count",
22
+ ]
23
+
24
+
25
+ def test_all_lists_the_public_api() -> None:
26
+ """Check that __all__ lists exactly the public API."""
27
+ assert textutils.__all__ == EXPECTED_ALL
28
+
29
+
30
+ @pytest.mark.parametrize("name", EXPECTED_ALL)
31
+ def test_public_names_are_importable(name: str) -> None:
32
+ """Check that every name in __all__ is importable."""
33
+ assert hasattr(textutils, name)
34
+
35
+
36
+ @pytest.mark.parametrize("function", ALL_FUNCTIONS, ids=lambda f: f.__name__)
37
+ @pytest.mark.parametrize(
38
+ "value, type_name",
39
+ [
40
+ pytest.param(None, "NoneType", id="none"),
41
+ pytest.param(42, "int", id="int"),
42
+ pytest.param(3.14, "float", id="float"),
43
+ pytest.param(["a"], "list", id="list"),
44
+ pytest.param({"k": "v"}, "dict", id="dict"),
45
+ pytest.param(b"bytes", "bytes", id="bytes"),
46
+ ],
47
+ )
48
+ def test_non_string_input_raises_type_error(
49
+ function: object, value: object, type_name: str
50
+ ) -> None:
51
+ """Every public function rejects non-string input with a helpful message."""
52
+ with pytest.raises(TypeError, match=f"must be str, not {type_name}"):
53
+ function(value)
54
+
55
+
56
+ def test_accepts_str_subclass() -> None:
57
+ """Check that public functions accept a str subclass."""
58
+
59
+ class MyStr(str):
60
+ pass
61
+
62
+ assert word_count(MyStr("hello world")) == 2
@@ -0,0 +1,88 @@
1
+ """Tests for textutils.transform."""
2
+
3
+ import pytest
4
+
5
+ from textutils import capitalize_words, reverse, slugify
6
+
7
+ # --- reverse ----------------------------------------------------------------
8
+
9
+ REVERSE_CASES = [
10
+ pytest.param("stressed", "desserts", id="reverses-word"),
11
+ pytest.param("ab c", "c ba", id="reverses-sentence"),
12
+ pytest.param("AbC", "CbA", id="mixed-case"),
13
+ pytest.param("", "", id="empty-string"),
14
+ pytest.param("a", "a", id="single-character"),
15
+ pytest.param("racecar", "racecar", id="palindrome-unchanged"),
16
+ pytest.param("a\tb\n", "\nb\ta", id="reverses-tabs-and-newlines"),
17
+ pytest.param("héllo", "olléh", id="unicode"),
18
+ pytest.param("a\u0301b", "b\u0301a", id="combining-mark-order-flips"),
19
+ pytest.param("abc" * 20_000, ("abc" * 20_000)[::-1], id="very-long-input"),
20
+ ]
21
+
22
+
23
+ @pytest.mark.parametrize("text, expected", REVERSE_CASES)
24
+ def test_reverse(text: str, expected: str) -> None:
25
+ """Return the characters in reverse order."""
26
+ assert reverse(text) == expected
27
+
28
+
29
+ def test_reverse_does_not_mutate_input() -> None:
30
+ """Return a new string and leave the input unchanged."""
31
+ original = "abc"
32
+ reverse(original)
33
+ assert original == "abc"
34
+
35
+
36
+ # --- capitalize_words -------------------------------------------------------
37
+
38
+ CAPITALIZE_WORDS_CASES = [
39
+ pytest.param("hello world", "Hello World", id="capitalizes-each-word"),
40
+ pytest.param("hello WORLD", "Hello WORLD", id="leaves-rest-unchanged"),
41
+ pytest.param("it's fine", "It's Fine", id="does-not-break-apostrophes"),
42
+ pytest.param("a\tb", "A\tB", id="tab-is-a-separator"),
43
+ pytest.param("", "", id="empty-string"),
44
+ pytest.param(
45
+ " hello world\nagain", " Hello World\nAgain", id="preserves-whitespace"
46
+ ),
47
+ pytest.param("Hello World", "Hello World", id="already-capitalized"),
48
+ pytest.param("héllo wörld", "Héllo Wörld", id="unicode-word-start"),
49
+ pytest.param("123 abc", "123 Abc", id="leading-digits"),
50
+ pytest.param("!!!", "!!!", id="punctuation-only"),
51
+ pytest.param("(hello)", "(hello)", id="not-after-whitespace"),
52
+ pytest.param("well-known", "Well-known", id="hyphen-not-a-separator"),
53
+ ]
54
+
55
+
56
+ @pytest.mark.parametrize("text, expected", CAPITALIZE_WORDS_CASES)
57
+ def test_capitalize_words(text: str, expected: str) -> None:
58
+ """Uppercase the first character of each word."""
59
+ assert capitalize_words(text) == expected
60
+
61
+
62
+ # --- slugify ----------------------------------------------------------------
63
+
64
+ SLUGIFY_CASES = [
65
+ pytest.param("Hello, World!", "hello-world", id="punctuation-to-hyphen"),
66
+ pytest.param(
67
+ " Hello, World! Open Source ",
68
+ "hello-world-open-source",
69
+ id="collapses-repeated-spaces",
70
+ ),
71
+ pytest.param("Python --- is fun", "python-is-fun", id="collapses-repeated-hyphens"),
72
+ pytest.param("Version 2.0 released", "version-2-0-released", id="keeps-digits"),
73
+ pytest.param("", "", id="empty-string"),
74
+ pytest.param("!!!", "", id="punctuation-only"),
75
+ pytest.param("café au lait", "caf-au-lait", id="strips-non-ascii"),
76
+ pytest.param("well-known", "well-known", id="keeps-existing-hyphen"),
77
+ pytest.param("foo_bar", "foo-bar", id="underscore-to-hyphen"),
78
+ pytest.param("HELLO", "hello", id="lowercases-input"),
79
+ pytest.param("a\tb\nc", "a-b-c", id="tabs-and-newlines"),
80
+ pytest.param("--Foo--", "foo", id="strips-leading-trailing-hyphens"),
81
+ pytest.param("a" * 100_000, "a" * 100_000, id="very-long-input"),
82
+ ]
83
+
84
+
85
+ @pytest.mark.parametrize("text, expected", SLUGIFY_CASES)
86
+ def test_slugify(text: str, expected: str) -> None:
87
+ """Return a lowercase, hyphen-separated slug."""
88
+ assert slugify(text) == expected
@@ -0,0 +1,30 @@
1
+ """Tests for the private textutils._validation module."""
2
+
3
+ import pytest
4
+
5
+ from textutils._validation import _ensure_str
6
+
7
+
8
+ @pytest.mark.parametrize(
9
+ "text", ["hello", "", " "], ids=["ascii", "empty", "whitespace"]
10
+ )
11
+ def test_ensure_str_accepts_strings(text: str) -> None:
12
+ """Return None for string input."""
13
+ assert _ensure_str(text) is None
14
+
15
+
16
+ @pytest.mark.parametrize(
17
+ "value, type_name",
18
+ [
19
+ pytest.param(None, "NoneType", id="none"),
20
+ pytest.param(42, "int", id="int"),
21
+ pytest.param(3.14, "float", id="float"),
22
+ pytest.param(["a"], "list", id="list"),
23
+ pytest.param({"k": "v"}, "dict", id="dict"),
24
+ pytest.param(b"bytes", "bytes", id="bytes"),
25
+ ],
26
+ )
27
+ def test_ensure_str_rejects_non_strings(value: object, type_name: str) -> None:
28
+ """Raise TypeError naming the offending type."""
29
+ with pytest.raises(TypeError, match=f"must be str, not {type_name}"):
30
+ _ensure_str(value)
@@ -0,0 +1,24 @@
1
+ """textutils: small, dependency-free helpers for common text tasks.
2
+
3
+ Public API:
4
+ ``word_count``, ``character_count``, ``reverse``, ``capitalize_words``,
5
+ ``slugify``.
6
+
7
+ Everything else (modules or names starting with ``_``) is internal and may
8
+ change without notice.
9
+ """
10
+
11
+ from importlib.metadata import version
12
+
13
+ from .counting import character_count, word_count
14
+ from .transform import capitalize_words, reverse, slugify
15
+
16
+ __all__ = [
17
+ "capitalize_words",
18
+ "character_count",
19
+ "reverse",
20
+ "slugify",
21
+ "word_count",
22
+ ]
23
+
24
+ __version__ = version("textutils-hs")
@@ -0,0 +1,7 @@
1
+ """Private input-validation helpers for textutils."""
2
+
3
+
4
+ def _ensure_str(text: object) -> None:
5
+ """Raise a ``TypeError`` if *text* is not a :class:`str`."""
6
+ if not isinstance(text, str):
7
+ raise TypeError(f"text must be str, not {type(text).__name__}")
@@ -0,0 +1,70 @@
1
+ """Word-counting helpers for textutils."""
2
+
3
+ from ._validation import _ensure_str
4
+
5
+ __all__ = ["word_count", "character_count"]
6
+
7
+
8
+ def word_count(text: str) -> int:
9
+ """Count the words in *text*.
10
+
11
+ Words are separated by any amount of whitespace, so runs of spaces,
12
+ tabs, and newlines all act as a single separator.
13
+
14
+ Parameters
15
+ ----------
16
+ text : str
17
+ The string to count words in.
18
+
19
+ Returns
20
+ -------
21
+ int
22
+ The number of words in *text*. An empty string returns ``0``.
23
+
24
+ Raises
25
+ ------
26
+ TypeError
27
+ If *text* is not a :class:`str`.
28
+
29
+ Examples
30
+ --------
31
+ >>> word_count("hello world")
32
+ 2
33
+ >>> word_count(" spaced out ")
34
+ 2
35
+ >>> word_count("")
36
+ 0
37
+ """
38
+ _ensure_str(text)
39
+ return len(text.split())
40
+
41
+
42
+ def character_count(text: str) -> int:
43
+ """Count the characters in *text*, spaces included.
44
+
45
+ Parameters
46
+ ----------
47
+ text : str
48
+ The string to count characters in.
49
+
50
+ Returns
51
+ -------
52
+ int
53
+ The number of characters in *text*. An empty string returns ``0``.
54
+
55
+ Raises
56
+ ------
57
+ TypeError
58
+ If *text* is not a :class:`str`.
59
+
60
+ Examples
61
+ --------
62
+ >>> character_count("hello")
63
+ 5
64
+ >>> character_count("hi there")
65
+ 8
66
+ >>> character_count("")
67
+ 0
68
+ """
69
+ _ensure_str(text)
70
+ return len(text)
@@ -0,0 +1,122 @@
1
+ """String-transforming helpers for textutils."""
2
+
3
+ import re
4
+
5
+ from ._validation import _ensure_str
6
+
7
+ __all__ = ["reverse", "capitalize_words", "slugify"]
8
+
9
+ _WORD_START = re.compile(r"(?<!\S)\w")
10
+
11
+
12
+ def reverse(text: str) -> str:
13
+ """Return *text* with its characters in reverse order.
14
+
15
+ Parameters
16
+ ----------
17
+ text : str
18
+ The string to reverse.
19
+
20
+ Returns
21
+ -------
22
+ str
23
+ A new string holding the characters of *text* in reverse order. An
24
+ empty string returns an empty string.
25
+
26
+ Raises
27
+ ------
28
+ TypeError
29
+ If *text* is not a :class:`str`.
30
+
31
+ Examples
32
+ --------
33
+ >>> reverse("stressed")
34
+ 'desserts'
35
+ >>> reverse("abc")
36
+ 'cba'
37
+ >>> reverse("")
38
+ ''
39
+ """
40
+ _ensure_str(text)
41
+ return text[::-1]
42
+
43
+
44
+ def capitalize_words(text: str) -> str:
45
+ """Uppercase the first character of every word in *text*.
46
+
47
+ Only the first character of each word is changed; the rest of each word
48
+ is left exactly as it was. Whitespace, including newlines, is preserved.
49
+
50
+ Parameters
51
+ ----------
52
+ text : str
53
+ The string to transform.
54
+
55
+ Returns
56
+ -------
57
+ str
58
+ A new string with the first character of each word uppercased. An
59
+ empty string returns an empty string.
60
+
61
+ Raises
62
+ ------
63
+ TypeError
64
+ If *text* is not a :class:`str`.
65
+
66
+ Examples
67
+ --------
68
+ >>> capitalize_words("hello world")
69
+ 'Hello World'
70
+ >>> capitalize_words("hello WORLD")
71
+ 'Hello WORLD'
72
+ >>> capitalize_words("it's fine")
73
+ "It's Fine"
74
+ >>> capitalize_words("")
75
+ ''
76
+ """
77
+ _ensure_str(text)
78
+ return _WORD_START.sub(lambda match: match.group(0).upper(), text)
79
+
80
+
81
+ def slugify(text: str) -> str:
82
+ """Turn *text* into a URL-friendly slug.
83
+
84
+ The input is lowercased, every run of characters outside ``a-z0-9``
85
+ (spaces, punctuation, underscores, accents, ...) is replaced by a single
86
+ hyphen, and leading/trailing hyphens are removed. Existing hyphens are
87
+ kept as separators, so ``"well-known"`` stays ``"well-known"`` while
88
+ ``"foo_bar"`` becomes ``"foo-bar"``.
89
+
90
+ Parameters
91
+ ----------
92
+ text : str
93
+ The string to slugify.
94
+
95
+ Returns
96
+ -------
97
+ str
98
+ A slug containing only lowercase ASCII letters, digits, and internal
99
+ hyphens. An empty string, or a string with no letters/digits,
100
+ returns ``""``.
101
+
102
+ Raises
103
+ ------
104
+ TypeError
105
+ If *text* is not a :class:`str`.
106
+
107
+ Examples
108
+ --------
109
+ >>> slugify("Hello, World!")
110
+ 'hello-world'
111
+ >>> slugify(" Hello, World! Open Source ")
112
+ 'hello-world-open-source'
113
+ >>> slugify("Python --- is fun")
114
+ 'python-is-fun'
115
+ >>> slugify("Version 2.0 released")
116
+ 'version-2-0-released'
117
+ >>> slugify("!!!")
118
+ ''
119
+ """
120
+ _ensure_str(text)
121
+ slug = re.sub(r"[^a-z0-9]+", "-", text.lower())
122
+ return slug.strip("-")
@@ -0,0 +1,93 @@
1
+ Metadata-Version: 2.4
2
+ Name: textutils-hs
3
+ Version: 0.1.0
4
+ Summary: Small, dependency-free helpers for common text tasks.
5
+ Author: Hamza Fikri
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/hamzasinko/textutils
8
+ Project-URL: Repository, https://github.com/hamzasinko/textutils
9
+ Project-URL: Issues, https://github.com/hamzasinko/textutils/issues
10
+ Project-URL: Changelog, https://github.com/hamzasinko/textutils/blob/main/CHANGELOG.md
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3 :: Only
16
+ Classifier: Programming Language :: Python :: 3.9
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Topic :: Text Processing
21
+ Requires-Python: >=3.9
22
+ Description-Content-Type: text/markdown
23
+ License-File: LICENSE
24
+ Provides-Extra: dev
25
+ Requires-Dist: pytest>=8.0; extra == "dev"
26
+ Requires-Dist: pytest-cov>=5.0; extra == "dev"
27
+ Requires-Dist: black>=24.0; extra == "dev"
28
+ Requires-Dist: ruff>=0.5; extra == "dev"
29
+ Requires-Dist: pre-commit>=3.7; extra == "dev"
30
+ Requires-Dist: build>=1.2; extra == "dev"
31
+ Requires-Dist: twine>=5.0; extra == "dev"
32
+ Dynamic: license-file
33
+
34
+ # textutils
35
+
36
+ Small, dependency-free Python helpers for common text tasks: counting words,
37
+ counting characters, reversing a string, and capitalizing each word. Every
38
+ function has type hints, a full docstring, and tests.
39
+
40
+ ## Features
41
+
42
+ - `word_count(text)` — number of words in a string.
43
+ - `character_count(text)` — number of characters in a string.
44
+ - `reverse(text)` — the string reversed.
45
+ - `capitalize_words(text)` — first letter of each word capitalized.
46
+ - `slugify(text)` — a URL-friendly slug (lowercase, hyphen-separated).
47
+
48
+ Edge cases are handled: empty strings return empty results, and non-string
49
+ input raises `TypeError`.
50
+
51
+ ## Installation
52
+
53
+ Requires Python 3.9 or newer. No third-party runtime dependencies.
54
+
55
+ Clone and install in editable mode with the development tools:
56
+
57
+ ```bash
58
+ git clone https://github.com/hamzasinko/textutils.git
59
+ cd textutils
60
+ python -m pip install -e ".[dev]"
61
+ ```
62
+
63
+ Run the tests:
64
+
65
+ ```bash
66
+ python -m pytest
67
+ ```
68
+
69
+ ## Usage
70
+
71
+ ```python
72
+ from textutils import word_count, character_count, reverse, capitalize_words, slugify
73
+
74
+ word_count("hello world") # 2
75
+ character_count("hello") # 5
76
+ reverse("stressed") # "desserts"
77
+ capitalize_words("hello world") # "Hello World"
78
+ slugify("Hello, World!") # "hello-world"
79
+ ```
80
+
81
+ ## Contributing
82
+
83
+ Contributions are welcome.
84
+
85
+ 1. Fork the repository on GitHub.
86
+ 2. Create a branch: `git checkout -b feature/my-change`.
87
+ 3. Make your change, keeping commits small and focused.
88
+ 4. Run the tests: `python -m pytest`.
89
+ 5. Push your branch and open a pull request.
90
+
91
+ ## License
92
+
93
+ Released under the MIT License. See [LICENSE](LICENSE) for details.
@@ -0,0 +1,16 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ tests/test_counting.py
5
+ tests/test_public_api.py
6
+ tests/test_transform.py
7
+ tests/test_validation.py
8
+ textutils/__init__.py
9
+ textutils/_validation.py
10
+ textutils/counting.py
11
+ textutils/transform.py
12
+ textutils_hs.egg-info/PKG-INFO
13
+ textutils_hs.egg-info/SOURCES.txt
14
+ textutils_hs.egg-info/dependency_links.txt
15
+ textutils_hs.egg-info/requires.txt
16
+ textutils_hs.egg-info/top_level.txt
@@ -0,0 +1,9 @@
1
+
2
+ [dev]
3
+ pytest>=8.0
4
+ pytest-cov>=5.0
5
+ black>=24.0
6
+ ruff>=0.5
7
+ pre-commit>=3.7
8
+ build>=1.2
9
+ twine>=5.0
@@ -0,0 +1 @@
1
+ textutils