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.
- textutils_hs-0.1.0/LICENSE +8 -0
- textutils_hs-0.1.0/PKG-INFO +93 -0
- textutils_hs-0.1.0/README.md +60 -0
- textutils_hs-0.1.0/pyproject.toml +75 -0
- textutils_hs-0.1.0/setup.cfg +4 -0
- textutils_hs-0.1.0/tests/test_counting.py +49 -0
- textutils_hs-0.1.0/tests/test_public_api.py +62 -0
- textutils_hs-0.1.0/tests/test_transform.py +88 -0
- textutils_hs-0.1.0/tests/test_validation.py +30 -0
- textutils_hs-0.1.0/textutils/__init__.py +24 -0
- textutils_hs-0.1.0/textutils/_validation.py +7 -0
- textutils_hs-0.1.0/textutils/counting.py +70 -0
- textutils_hs-0.1.0/textutils/transform.py +122 -0
- textutils_hs-0.1.0/textutils_hs.egg-info/PKG-INFO +93 -0
- textutils_hs-0.1.0/textutils_hs.egg-info/SOURCES.txt +16 -0
- textutils_hs-0.1.0/textutils_hs.egg-info/dependency_links.txt +1 -0
- textutils_hs-0.1.0/textutils_hs.egg-info/requires.txt +9 -0
- textutils_hs-0.1.0/textutils_hs.egg-info/top_level.txt +1 -0
|
@@ -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,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,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 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
textutils
|