better-profanity-rs 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.
- better_profanity_rs-0.1.0/Cargo.lock +147 -0
- better_profanity_rs-0.1.0/Cargo.toml +16 -0
- better_profanity_rs-0.1.0/LICENSE +22 -0
- better_profanity_rs-0.1.0/NOTICE +18 -0
- better_profanity_rs-0.1.0/PKG-INFO +188 -0
- better_profanity_rs-0.1.0/README.md +167 -0
- better_profanity_rs-0.1.0/bindings/python/Cargo.toml +20 -0
- better_profanity_rs-0.1.0/bindings/python/LICENSE +22 -0
- better_profanity_rs-0.1.0/bindings/python/NOTICE +18 -0
- better_profanity_rs-0.1.0/bindings/python/README.md +167 -0
- better_profanity_rs-0.1.0/bindings/python/benchmarks/compare.py +135 -0
- better_profanity_rs-0.1.0/bindings/python/benchmarks/data.py +62 -0
- better_profanity_rs-0.1.0/bindings/python/benchmarks/parity.py +139 -0
- better_profanity_rs-0.1.0/bindings/python/benchmarks/results/m1-pro.json +94 -0
- better_profanity_rs-0.1.0/bindings/python/benchmarks/results/parity-3.11.json +67 -0
- better_profanity_rs-0.1.0/bindings/python/benchmarks/results/parity-3.13.json +67 -0
- better_profanity_rs-0.1.0/bindings/python/benchmarks/results/parity-3.14.json +67 -0
- better_profanity_rs-0.1.0/bindings/python/benchmarks/results/parity-3.9.json +67 -0
- better_profanity_rs-0.1.0/bindings/python/src/lib.rs +452 -0
- better_profanity_rs-0.1.0/bindings/python/tests/conftest.py +35 -0
- better_profanity_rs-0.1.0/bindings/python/tests/inputs.py +79 -0
- better_profanity_rs-0.1.0/bindings/python/tests/test_parity.py +976 -0
- better_profanity_rs-0.1.0/bindings/python/tests/test_upstream.py +211 -0
- better_profanity_rs-0.1.0/bindings/python/uv.lock +654 -0
- better_profanity_rs-0.1.0/crates/better-profanity/Cargo.toml +17 -0
- better_profanity_rs-0.1.0/crates/better-profanity/LICENSE +22 -0
- better_profanity_rs-0.1.0/crates/better-profanity/NOTICE +9 -0
- better_profanity_rs-0.1.0/crates/better-profanity/README.md +167 -0
- better_profanity_rs-0.1.0/crates/better-profanity/data/alphabetic_unicode.json +1 -0
- better_profanity_rs-0.1.0/crates/better-profanity/data/profanity_wordlist.txt +916 -0
- better_profanity_rs-0.1.0/crates/better-profanity/src/censor.rs +262 -0
- better_profanity_rs-0.1.0/crates/better-profanity/src/charset.rs +62 -0
- better_profanity_rs-0.1.0/crates/better-profanity/src/data.rs +134 -0
- better_profanity_rs-0.1.0/crates/better-profanity/src/lib.rs +394 -0
- better_profanity_rs-0.1.0/crates/better-profanity/src/words.rs +221 -0
- better_profanity_rs-0.1.0/pyproject.toml +66 -0
- better_profanity_rs-0.1.0/python/better_profanity_rs/__init__.py +27 -0
- better_profanity_rs-0.1.0/python/better_profanity_rs/_fast.py +294 -0
- better_profanity_rs-0.1.0/python/better_profanity_rs/_original.py +64 -0
- better_profanity_rs-0.1.0/python/better_profanity_rs/alphabetic_unicode.json +1 -0
- better_profanity_rs-0.1.0/python/better_profanity_rs/better_profanity.py +256 -0
- better_profanity_rs-0.1.0/python/better_profanity_rs/constants.py +15 -0
- better_profanity_rs-0.1.0/python/better_profanity_rs/profanity_wordlist.txt +916 -0
- better_profanity_rs-0.1.0/python/better_profanity_rs/utils.py +47 -0
- better_profanity_rs-0.1.0/python/better_profanity_rs/varying_string.py +67 -0
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# This file is automatically @generated by Cargo.
|
|
2
|
+
# It is not intended for manual editing.
|
|
3
|
+
version = 4
|
|
4
|
+
|
|
5
|
+
[[package]]
|
|
6
|
+
name = "better-profanity"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
dependencies = [
|
|
9
|
+
"rustc-hash",
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
[[package]]
|
|
13
|
+
name = "better-profanity-python"
|
|
14
|
+
version = "0.1.0"
|
|
15
|
+
dependencies = [
|
|
16
|
+
"better-profanity",
|
|
17
|
+
"pyo3",
|
|
18
|
+
"rustc-hash",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
[[package]]
|
|
22
|
+
name = "heck"
|
|
23
|
+
version = "0.5.0"
|
|
24
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
25
|
+
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
|
26
|
+
|
|
27
|
+
[[package]]
|
|
28
|
+
name = "libc"
|
|
29
|
+
version = "0.2.191"
|
|
30
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
31
|
+
checksum = "65cd87c9a6d60536580b316ad985726104730a5151c99845f2490428ef91240a"
|
|
32
|
+
|
|
33
|
+
[[package]]
|
|
34
|
+
name = "once_cell"
|
|
35
|
+
version = "1.21.4"
|
|
36
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
37
|
+
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
|
38
|
+
|
|
39
|
+
[[package]]
|
|
40
|
+
name = "portable-atomic"
|
|
41
|
+
version = "1.15.0"
|
|
42
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
43
|
+
checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85"
|
|
44
|
+
|
|
45
|
+
[[package]]
|
|
46
|
+
name = "proc-macro2"
|
|
47
|
+
version = "1.0.107"
|
|
48
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
49
|
+
checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
|
|
50
|
+
dependencies = [
|
|
51
|
+
"unicode-ident",
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
[[package]]
|
|
55
|
+
name = "pyo3"
|
|
56
|
+
version = "0.29.3"
|
|
57
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
58
|
+
checksum = "700d18fa267b73b9b521fd7e13580e2f446916f176cacee1ab63fcc8191f1655"
|
|
59
|
+
dependencies = [
|
|
60
|
+
"libc",
|
|
61
|
+
"once_cell",
|
|
62
|
+
"portable-atomic",
|
|
63
|
+
"pyo3-build-config",
|
|
64
|
+
"pyo3-ffi",
|
|
65
|
+
"pyo3-macros",
|
|
66
|
+
]
|
|
67
|
+
|
|
68
|
+
[[package]]
|
|
69
|
+
name = "pyo3-build-config"
|
|
70
|
+
version = "0.29.3"
|
|
71
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
72
|
+
checksum = "7b3fc0c4d08f6bb10e71fe39dfb9e2f59c6eb6854e22ec8092f50c69a4499adb"
|
|
73
|
+
dependencies = [
|
|
74
|
+
"target-lexicon",
|
|
75
|
+
]
|
|
76
|
+
|
|
77
|
+
[[package]]
|
|
78
|
+
name = "pyo3-ffi"
|
|
79
|
+
version = "0.29.3"
|
|
80
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
81
|
+
checksum = "dfc0b8e19df29aad7086cf977bb0c2a2f143e30567eb113e9cf72b62ca698330"
|
|
82
|
+
dependencies = [
|
|
83
|
+
"libc",
|
|
84
|
+
"pyo3-build-config",
|
|
85
|
+
]
|
|
86
|
+
|
|
87
|
+
[[package]]
|
|
88
|
+
name = "pyo3-macros"
|
|
89
|
+
version = "0.29.3"
|
|
90
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
91
|
+
checksum = "6100e8a4b5eba53afaa5ed078364851a0b2499c553a44c31026b929049b49dc6"
|
|
92
|
+
dependencies = [
|
|
93
|
+
"proc-macro2",
|
|
94
|
+
"pyo3-macros-backend",
|
|
95
|
+
"quote",
|
|
96
|
+
"syn",
|
|
97
|
+
]
|
|
98
|
+
|
|
99
|
+
[[package]]
|
|
100
|
+
name = "pyo3-macros-backend"
|
|
101
|
+
version = "0.29.3"
|
|
102
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
103
|
+
checksum = "6143877a16e82b5a727b7127ff4cd86858a24a28d745d72f43e6f227c7b1bdb3"
|
|
104
|
+
dependencies = [
|
|
105
|
+
"heck",
|
|
106
|
+
"proc-macro2",
|
|
107
|
+
"quote",
|
|
108
|
+
"syn",
|
|
109
|
+
]
|
|
110
|
+
|
|
111
|
+
[[package]]
|
|
112
|
+
name = "quote"
|
|
113
|
+
version = "1.0.47"
|
|
114
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
115
|
+
checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
|
|
116
|
+
dependencies = [
|
|
117
|
+
"proc-macro2",
|
|
118
|
+
]
|
|
119
|
+
|
|
120
|
+
[[package]]
|
|
121
|
+
name = "rustc-hash"
|
|
122
|
+
version = "2.1.3"
|
|
123
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
124
|
+
checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d"
|
|
125
|
+
|
|
126
|
+
[[package]]
|
|
127
|
+
name = "syn"
|
|
128
|
+
version = "2.0.119"
|
|
129
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
130
|
+
checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
|
|
131
|
+
dependencies = [
|
|
132
|
+
"proc-macro2",
|
|
133
|
+
"quote",
|
|
134
|
+
"unicode-ident",
|
|
135
|
+
]
|
|
136
|
+
|
|
137
|
+
[[package]]
|
|
138
|
+
name = "target-lexicon"
|
|
139
|
+
version = "0.13.5"
|
|
140
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
141
|
+
checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca"
|
|
142
|
+
|
|
143
|
+
[[package]]
|
|
144
|
+
name = "unicode-ident"
|
|
145
|
+
version = "1.0.27"
|
|
146
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
147
|
+
checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563"
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
[workspace]
|
|
2
|
+
members = ["crates/better-profanity", "bindings/python"]
|
|
3
|
+
resolver = "2"
|
|
4
|
+
|
|
5
|
+
[workspace.package]
|
|
6
|
+
version = "0.1.0"
|
|
7
|
+
authors = ["Alex Kholodniak <alexandrkholodniak@gmail.com>"]
|
|
8
|
+
edition = "2021"
|
|
9
|
+
rust-version = "1.85"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
repository = "https://github.com/SyntaxSpirits/better-profanity-rs"
|
|
12
|
+
homepage = "https://syntaxspirits.dev"
|
|
13
|
+
|
|
14
|
+
[profile.release]
|
|
15
|
+
lto = "thin"
|
|
16
|
+
codegen-units = 1
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Alex Kholodniak
|
|
4
|
+
Copyright (c) 2018 The Python Packaging Authority
|
|
5
|
+
|
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
8
|
+
in the Software without restriction, including without limitation the rights
|
|
9
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
10
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
11
|
+
furnished to do so, subject to the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be included in all
|
|
14
|
+
copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
19
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
21
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
22
|
+
SOFTWARE.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
better-profanity-rs
|
|
2
|
+
Copyright 2026 Alex Kholodniak
|
|
3
|
+
|
|
4
|
+
This product is a port of better_profanity (https://github.com/snguyenthanh/better_profanity)
|
|
5
|
+
by Son Nguyen Thanh, licensed under the MIT License. Its LICENSE file reads
|
|
6
|
+
"Copyright (c) 2018 The Python Packaging Authority"; that notice is kept in LICENSE.
|
|
7
|
+
|
|
8
|
+
The following files are copies of better_profanity 0.7.0, unchanged:
|
|
9
|
+
|
|
10
|
+
- crates/better-profanity/data/profanity_wordlist.txt and
|
|
11
|
+
bindings/python/python/better_profanity_rs/profanity_wordlist.txt (the word list);
|
|
12
|
+
- crates/better-profanity/data/alphabetic_unicode.json and
|
|
13
|
+
bindings/python/python/better_profanity_rs/alphabetic_unicode.json;
|
|
14
|
+
- bindings/python/python/better_profanity_rs/better_profanity.py, constants.py, utils.py and
|
|
15
|
+
varying_string.py, which the Python package runs for inputs the Rust core does not handle.
|
|
16
|
+
|
|
17
|
+
bindings/python/python/better_profanity_rs/_original.py holds the source of
|
|
18
|
+
Profanity._hide_swear_words from better_profanity 0.7.0 as a string, unchanged.
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: better-profanity-rs
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Classifier: Development Status :: 4 - Beta
|
|
5
|
+
Classifier: Intended Audience :: Developers
|
|
6
|
+
Classifier: Programming Language :: Python :: 3
|
|
7
|
+
Classifier: Programming Language :: Rust
|
|
8
|
+
Classifier: Topic :: Text Processing :: Filters
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
License-File: NOTICE
|
|
11
|
+
Summary: Fast drop-in replacement for better_profanity: the same censored text from a Rust core
|
|
12
|
+
Keywords: profanity,better_profanity,censor,swear-words,moderation,rust
|
|
13
|
+
Author-email: Alex Kholodniak <alexandrkholodniak@gmail.com>
|
|
14
|
+
License-Expression: MIT
|
|
15
|
+
Requires-Python: >=3.9
|
|
16
|
+
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
|
|
17
|
+
Project-URL: Homepage, https://syntaxspirits.dev
|
|
18
|
+
Project-URL: Issues, https://github.com/SyntaxSpirits/better-profanity-rs/issues
|
|
19
|
+
Project-URL: Repository, https://github.com/SyntaxSpirits/better-profanity-rs
|
|
20
|
+
|
|
21
|
+
# better-profanity-rs
|
|
22
|
+
|
|
23
|
+
[](https://github.com/SyntaxSpirits/better-profanity-rs/actions/workflows/ci.yml)
|
|
24
|
+
[](https://pypi.org/project/better-profanity-rs/)
|
|
25
|
+
[](https://crates.io/crates/better-profanity)
|
|
26
|
+
[](LICENSE)
|
|
27
|
+
|
|
28
|
+
[`better_profanity`](https://pypi.org/project/better-profanity/) with a Rust core: the same
|
|
29
|
+
censored text for every input, about 2,100 times faster on a 10-word message and
|
|
30
|
+
10,000 to 12,000 times faster on texts of 100 to 700 words
|
|
31
|
+
([benchmarks](https://github.com/SyntaxSpirits/better-profanity-rs/blob/main/BENCHMARKS.md)).
|
|
32
|
+
|
|
33
|
+
`better_profanity` is downloaded about two million times a month, although its last release, 0.7.0,
|
|
34
|
+
dates from November 2020. It is slow: `contains_profanity` takes 9 ms for a 10-word message and
|
|
35
|
+
0.9 s for a 700-word article on an M1 Pro with CPython 3.13. The word list is a Python list of 916
|
|
36
|
+
objects, and each word of the text, alone and joined with the words after it, is compared with every
|
|
37
|
+
one of them by a method that tries the leetspeak variants letter by letter. `better-profanity-rs`
|
|
38
|
+
turns the word list into a trie whose edges are sets of characters (`a`, `@`, `*` and `4` for an
|
|
39
|
+
`a`) and runs the original scan, step for step, in Rust. Words in leetspeak, phrases split by
|
|
40
|
+
separators (`f.u.c.k`, `H4nd j0b`), whitelists, custom word lists and the quirks of the original
|
|
41
|
+
give the same output.
|
|
42
|
+
|
|
43
|
+
The Python package is `better-profanity-rs`; the Rust crate is `better-profanity`. This is an
|
|
44
|
+
independent project, not affiliated with the author of `better_profanity`.
|
|
45
|
+
|
|
46
|
+
## Python
|
|
47
|
+
|
|
48
|
+
```sh
|
|
49
|
+
pip install better-profanity-rs
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Change the import and keep the rest of your code:
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
from better_profanity_rs import profanity # was: from better_profanity import profanity
|
|
56
|
+
|
|
57
|
+
profanity.censor("That wh0re gave m3 a very good H4nd j0b, dude.")
|
|
58
|
+
# 'That **** gave m3 a very good ****, dude.'
|
|
59
|
+
profanity.contains_profanity("he is a m0th3rf*cker")
|
|
60
|
+
# True
|
|
61
|
+
profanity.load_censor_words(["happy", "jolly"], whitelist_words=["jolly"])
|
|
62
|
+
profanity.censor("Happy and jolly days", "-")
|
|
63
|
+
# '---- and jolly days'
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
`Profanity`, `load_censor_words`, `load_censor_words_from_file`, `add_censor_words`, `censor`
|
|
67
|
+
and `contains_profanity` take the same arguments as in `better_profanity` 0.7.0 and return the
|
|
68
|
+
same results or raise the same exceptions. The attributes `CENSOR_WORDSET`, `CHARS_MAPPING`,
|
|
69
|
+
`MAX_NUMBER_COMBINATIONS` and `ALLOWED_CHARACTERS` can be read and changed as before, and the
|
|
70
|
+
modules `better_profanity`, `constants`, `utils` and `varying_string` are those of 0.7.0.
|
|
71
|
+
|
|
72
|
+
### Code you do not control
|
|
73
|
+
|
|
74
|
+
When a library imports `better_profanity` itself, call `install()` before importing it:
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
import better_profanity_rs
|
|
78
|
+
|
|
79
|
+
better_profanity_rs.install() # `import better_profanity` now returns better_profanity_rs
|
|
80
|
+
|
|
81
|
+
import my_web_app
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
`install()` covers `better_profanity` and its modules `better_profanity`, `constants`, `utils`
|
|
85
|
+
and `varying_string`. Modules that imported `better_profanity` before the call keep the
|
|
86
|
+
original.
|
|
87
|
+
|
|
88
|
+
## Rust
|
|
89
|
+
|
|
90
|
+
```toml
|
|
91
|
+
[dependencies]
|
|
92
|
+
better-profanity = "0.1"
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
```rust
|
|
96
|
+
use better_profanity::Profanity;
|
|
97
|
+
|
|
98
|
+
let profanity = Profanity::new();
|
|
99
|
+
assert_eq!(
|
|
100
|
+
profanity.censor("That wh0re gave m3 a very good H@nD j0b.", "*"),
|
|
101
|
+
"That **** gave m3 a very good ****."
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
let mut custom = Profanity::with_words(["happy", "jolly"]);
|
|
105
|
+
custom.add_censor_words(["merry christmas"]);
|
|
106
|
+
assert!(custom.contains_profanity("Merry Christmas!"));
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
`Profanity` follows the Python class; `hide_swear_words`, `WordSet` and `CharSet` expose the
|
|
110
|
+
scan for other word sets, separator sets and lowercasing rules. The crate lowercases with Rust's
|
|
111
|
+
Unicode tables, which can differ from those of a given Python for characters added to Unicode
|
|
112
|
+
recently; the Python package always uses the running interpreter's `str.lower()`.
|
|
113
|
+
|
|
114
|
+
## Compatibility
|
|
115
|
+
|
|
116
|
+
The tests compare `better-profanity-rs` with `better_profanity` 0.7.0 and fail on any
|
|
117
|
+
difference in output or exception: the original's own test suite, hand-picked cases,
|
|
118
|
+
dictionary words in leetspeak inside texts and joined by separators, custom word lists and
|
|
119
|
+
whitelists, invalid arguments, every kind of attribute change listed below, subclasses,
|
|
120
|
+
pickling, threads and property-based tests, on CPython 3.9 to 3.14. A separate script compares
|
|
121
|
+
285,074 texts, among them the civil_comments test split and the Jigsaw toxic comments (50,532
|
|
122
|
+
of the texts are censored), and 28,183 of them again with a custom word list and whitelist, on
|
|
123
|
+
CPython 3.9, 3.11, 3.13 and 3.14: no output differs (see [BENCHMARKS.md](BENCHMARKS.md)).
|
|
124
|
+
|
|
125
|
+
This includes the original's quirks. For example, `add_censor_words` does not raise
|
|
126
|
+
`MAX_NUMBER_COMBINATIONS`, and when it is 1, only every other word is joined to the next one.
|
|
127
|
+
After `Profanity(["happy", "jolly"]).add_censor_words(["merry christmas"])`, both packages
|
|
128
|
+
censor "m3rry christmas" and "a b m3rry christmas" but not "a m3rry christmas".
|
|
129
|
+
|
|
130
|
+
`better_profanity`'s own code, bundled in the package, runs whenever the Rust core could
|
|
131
|
+
behave differently, so the output stays the same and only the speed-up is lost:
|
|
132
|
+
|
|
133
|
+
- the text or `censor_char` is not an exact `str` (a `str` subclass, for example), or contains
|
|
134
|
+
lone surrogates;
|
|
135
|
+
- `CENSOR_WORDSET` is not an exact list or tuple of `VaryingString` objects and strings, or a
|
|
136
|
+
`CHARS_MAPPING` value used by a word has a variant that is not exactly one character
|
|
137
|
+
(`better_profanity` also accepts longer and empty variants);
|
|
138
|
+
- `MAX_NUMBER_COMBINATIONS` is not an `int`, or the call is made so close to the recursion
|
|
139
|
+
limit that the original, which recurses `MAX_NUMBER_COMBINATIONS` calls deep, could reach
|
|
140
|
+
it (within about twice `MAX_NUMBER_COMBINATIONS` + 20 frames);
|
|
141
|
+
- `ALLOWED_CHARACTERS` was replaced by another set, or holds something other than strings;
|
|
142
|
+
- a subclass or a patch replaces `_get_next_words`, `_update_next_words_indices`,
|
|
143
|
+
`_get_start_index_of_next_word`, `_get_next_word_and_end_index`,
|
|
144
|
+
`get_replacement_for_swear_word`, `any_next_words_form_swear_word` or
|
|
145
|
+
`VaryingString.__eq__`, or turns one of the attributes into a property.
|
|
146
|
+
|
|
147
|
+
The original runs in the same stack frame as in `better_profanity`, so a `RecursionError` is
|
|
148
|
+
raised exactly where `better_profanity` raises it. Changes in place are seen. `CENSOR_WORDSET` is compiled again when its items change: each call
|
|
149
|
+
checks that the list still holds the same objects, which takes about 1 µs for the default
|
|
150
|
+
list. `ALLOWED_CHARACTERS` is a `set` subclass that notes its changes. Lowercasing uses the
|
|
151
|
+
interpreter's `str.lower()`, so, as with `better_profanity`, results follow the Unicode version
|
|
152
|
+
of the Python running them.
|
|
153
|
+
|
|
154
|
+
Known differences:
|
|
155
|
+
|
|
156
|
+
- The private attributes of a `VaryingString` (`_char_combos`, `_min_len`, `_max_len`) are
|
|
157
|
+
read when the word list is compiled; changing them later on an object already in
|
|
158
|
+
`CENSOR_WORDSET` is not seen.
|
|
159
|
+
- `ALLOWED_CHARACTERS` is a `set` subclass; changes made through `set` itself, as in
|
|
160
|
+
`set.add(ALLOWED_CHARACTERS, "-")`, are not seen.
|
|
161
|
+
- Near the recursion limit, frames are counted with `sys._getframe`. CPython 3.11 and older
|
|
162
|
+
also count nested C calls toward the limit; the check allows one per Python frame, so a call
|
|
163
|
+
made deep inside C-level recursion (a recursive `__repr__`, for example) could still raise
|
|
164
|
+
where `better_profanity` would not, or the reverse. On CPython 3.11, a call made within two
|
|
165
|
+
frames of the limit raises `RecursionError` as `better_profanity` does, but the message can
|
|
166
|
+
name a different operation.
|
|
167
|
+
- A compiled word list keeps the `VaryingString` objects of `CENSOR_WORDSET` alive until the
|
|
168
|
+
list changes or the `Profanity` object is deleted. An attribute added to one of them that
|
|
169
|
+
refers back to the `Profanity` object keeps that object alive.
|
|
170
|
+
- Free-threaded CPython builds are not supported: the extension module asks to keep the GIL
|
|
171
|
+
enabled.
|
|
172
|
+
- `__version__` and `name` are those of `better_profanity_rs`.
|
|
173
|
+
|
|
174
|
+
## Development
|
|
175
|
+
|
|
176
|
+
```sh
|
|
177
|
+
cargo test -p better-profanity
|
|
178
|
+
cd bindings/python
|
|
179
|
+
uv sync --group dev
|
|
180
|
+
uv run pytest
|
|
181
|
+
cd benchmarks && uv run --group bench python parity.py # all corpora
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
## License
|
|
185
|
+
|
|
186
|
+
MIT, the same licence as `better_profanity`, whose code, word list and character list this
|
|
187
|
+
project includes. See [NOTICE](NOTICE).
|
|
188
|
+
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# better-profanity-rs
|
|
2
|
+
|
|
3
|
+
[](https://github.com/SyntaxSpirits/better-profanity-rs/actions/workflows/ci.yml)
|
|
4
|
+
[](https://pypi.org/project/better-profanity-rs/)
|
|
5
|
+
[](https://crates.io/crates/better-profanity)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
|
|
8
|
+
[`better_profanity`](https://pypi.org/project/better-profanity/) with a Rust core: the same
|
|
9
|
+
censored text for every input, about 2,100 times faster on a 10-word message and
|
|
10
|
+
10,000 to 12,000 times faster on texts of 100 to 700 words
|
|
11
|
+
([benchmarks](https://github.com/SyntaxSpirits/better-profanity-rs/blob/main/BENCHMARKS.md)).
|
|
12
|
+
|
|
13
|
+
`better_profanity` is downloaded about two million times a month, although its last release, 0.7.0,
|
|
14
|
+
dates from November 2020. It is slow: `contains_profanity` takes 9 ms for a 10-word message and
|
|
15
|
+
0.9 s for a 700-word article on an M1 Pro with CPython 3.13. The word list is a Python list of 916
|
|
16
|
+
objects, and each word of the text, alone and joined with the words after it, is compared with every
|
|
17
|
+
one of them by a method that tries the leetspeak variants letter by letter. `better-profanity-rs`
|
|
18
|
+
turns the word list into a trie whose edges are sets of characters (`a`, `@`, `*` and `4` for an
|
|
19
|
+
`a`) and runs the original scan, step for step, in Rust. Words in leetspeak, phrases split by
|
|
20
|
+
separators (`f.u.c.k`, `H4nd j0b`), whitelists, custom word lists and the quirks of the original
|
|
21
|
+
give the same output.
|
|
22
|
+
|
|
23
|
+
The Python package is `better-profanity-rs`; the Rust crate is `better-profanity`. This is an
|
|
24
|
+
independent project, not affiliated with the author of `better_profanity`.
|
|
25
|
+
|
|
26
|
+
## Python
|
|
27
|
+
|
|
28
|
+
```sh
|
|
29
|
+
pip install better-profanity-rs
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Change the import and keep the rest of your code:
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
from better_profanity_rs import profanity # was: from better_profanity import profanity
|
|
36
|
+
|
|
37
|
+
profanity.censor("That wh0re gave m3 a very good H4nd j0b, dude.")
|
|
38
|
+
# 'That **** gave m3 a very good ****, dude.'
|
|
39
|
+
profanity.contains_profanity("he is a m0th3rf*cker")
|
|
40
|
+
# True
|
|
41
|
+
profanity.load_censor_words(["happy", "jolly"], whitelist_words=["jolly"])
|
|
42
|
+
profanity.censor("Happy and jolly days", "-")
|
|
43
|
+
# '---- and jolly days'
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
`Profanity`, `load_censor_words`, `load_censor_words_from_file`, `add_censor_words`, `censor`
|
|
47
|
+
and `contains_profanity` take the same arguments as in `better_profanity` 0.7.0 and return the
|
|
48
|
+
same results or raise the same exceptions. The attributes `CENSOR_WORDSET`, `CHARS_MAPPING`,
|
|
49
|
+
`MAX_NUMBER_COMBINATIONS` and `ALLOWED_CHARACTERS` can be read and changed as before, and the
|
|
50
|
+
modules `better_profanity`, `constants`, `utils` and `varying_string` are those of 0.7.0.
|
|
51
|
+
|
|
52
|
+
### Code you do not control
|
|
53
|
+
|
|
54
|
+
When a library imports `better_profanity` itself, call `install()` before importing it:
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
import better_profanity_rs
|
|
58
|
+
|
|
59
|
+
better_profanity_rs.install() # `import better_profanity` now returns better_profanity_rs
|
|
60
|
+
|
|
61
|
+
import my_web_app
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
`install()` covers `better_profanity` and its modules `better_profanity`, `constants`, `utils`
|
|
65
|
+
and `varying_string`. Modules that imported `better_profanity` before the call keep the
|
|
66
|
+
original.
|
|
67
|
+
|
|
68
|
+
## Rust
|
|
69
|
+
|
|
70
|
+
```toml
|
|
71
|
+
[dependencies]
|
|
72
|
+
better-profanity = "0.1"
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
```rust
|
|
76
|
+
use better_profanity::Profanity;
|
|
77
|
+
|
|
78
|
+
let profanity = Profanity::new();
|
|
79
|
+
assert_eq!(
|
|
80
|
+
profanity.censor("That wh0re gave m3 a very good H@nD j0b.", "*"),
|
|
81
|
+
"That **** gave m3 a very good ****."
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
let mut custom = Profanity::with_words(["happy", "jolly"]);
|
|
85
|
+
custom.add_censor_words(["merry christmas"]);
|
|
86
|
+
assert!(custom.contains_profanity("Merry Christmas!"));
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
`Profanity` follows the Python class; `hide_swear_words`, `WordSet` and `CharSet` expose the
|
|
90
|
+
scan for other word sets, separator sets and lowercasing rules. The crate lowercases with Rust's
|
|
91
|
+
Unicode tables, which can differ from those of a given Python for characters added to Unicode
|
|
92
|
+
recently; the Python package always uses the running interpreter's `str.lower()`.
|
|
93
|
+
|
|
94
|
+
## Compatibility
|
|
95
|
+
|
|
96
|
+
The tests compare `better-profanity-rs` with `better_profanity` 0.7.0 and fail on any
|
|
97
|
+
difference in output or exception: the original's own test suite, hand-picked cases,
|
|
98
|
+
dictionary words in leetspeak inside texts and joined by separators, custom word lists and
|
|
99
|
+
whitelists, invalid arguments, every kind of attribute change listed below, subclasses,
|
|
100
|
+
pickling, threads and property-based tests, on CPython 3.9 to 3.14. A separate script compares
|
|
101
|
+
285,074 texts, among them the civil_comments test split and the Jigsaw toxic comments (50,532
|
|
102
|
+
of the texts are censored), and 28,183 of them again with a custom word list and whitelist, on
|
|
103
|
+
CPython 3.9, 3.11, 3.13 and 3.14: no output differs (see [BENCHMARKS.md](BENCHMARKS.md)).
|
|
104
|
+
|
|
105
|
+
This includes the original's quirks. For example, `add_censor_words` does not raise
|
|
106
|
+
`MAX_NUMBER_COMBINATIONS`, and when it is 1, only every other word is joined to the next one.
|
|
107
|
+
After `Profanity(["happy", "jolly"]).add_censor_words(["merry christmas"])`, both packages
|
|
108
|
+
censor "m3rry christmas" and "a b m3rry christmas" but not "a m3rry christmas".
|
|
109
|
+
|
|
110
|
+
`better_profanity`'s own code, bundled in the package, runs whenever the Rust core could
|
|
111
|
+
behave differently, so the output stays the same and only the speed-up is lost:
|
|
112
|
+
|
|
113
|
+
- the text or `censor_char` is not an exact `str` (a `str` subclass, for example), or contains
|
|
114
|
+
lone surrogates;
|
|
115
|
+
- `CENSOR_WORDSET` is not an exact list or tuple of `VaryingString` objects and strings, or a
|
|
116
|
+
`CHARS_MAPPING` value used by a word has a variant that is not exactly one character
|
|
117
|
+
(`better_profanity` also accepts longer and empty variants);
|
|
118
|
+
- `MAX_NUMBER_COMBINATIONS` is not an `int`, or the call is made so close to the recursion
|
|
119
|
+
limit that the original, which recurses `MAX_NUMBER_COMBINATIONS` calls deep, could reach
|
|
120
|
+
it (within about twice `MAX_NUMBER_COMBINATIONS` + 20 frames);
|
|
121
|
+
- `ALLOWED_CHARACTERS` was replaced by another set, or holds something other than strings;
|
|
122
|
+
- a subclass or a patch replaces `_get_next_words`, `_update_next_words_indices`,
|
|
123
|
+
`_get_start_index_of_next_word`, `_get_next_word_and_end_index`,
|
|
124
|
+
`get_replacement_for_swear_word`, `any_next_words_form_swear_word` or
|
|
125
|
+
`VaryingString.__eq__`, or turns one of the attributes into a property.
|
|
126
|
+
|
|
127
|
+
The original runs in the same stack frame as in `better_profanity`, so a `RecursionError` is
|
|
128
|
+
raised exactly where `better_profanity` raises it. Changes in place are seen. `CENSOR_WORDSET` is compiled again when its items change: each call
|
|
129
|
+
checks that the list still holds the same objects, which takes about 1 µs for the default
|
|
130
|
+
list. `ALLOWED_CHARACTERS` is a `set` subclass that notes its changes. Lowercasing uses the
|
|
131
|
+
interpreter's `str.lower()`, so, as with `better_profanity`, results follow the Unicode version
|
|
132
|
+
of the Python running them.
|
|
133
|
+
|
|
134
|
+
Known differences:
|
|
135
|
+
|
|
136
|
+
- The private attributes of a `VaryingString` (`_char_combos`, `_min_len`, `_max_len`) are
|
|
137
|
+
read when the word list is compiled; changing them later on an object already in
|
|
138
|
+
`CENSOR_WORDSET` is not seen.
|
|
139
|
+
- `ALLOWED_CHARACTERS` is a `set` subclass; changes made through `set` itself, as in
|
|
140
|
+
`set.add(ALLOWED_CHARACTERS, "-")`, are not seen.
|
|
141
|
+
- Near the recursion limit, frames are counted with `sys._getframe`. CPython 3.11 and older
|
|
142
|
+
also count nested C calls toward the limit; the check allows one per Python frame, so a call
|
|
143
|
+
made deep inside C-level recursion (a recursive `__repr__`, for example) could still raise
|
|
144
|
+
where `better_profanity` would not, or the reverse. On CPython 3.11, a call made within two
|
|
145
|
+
frames of the limit raises `RecursionError` as `better_profanity` does, but the message can
|
|
146
|
+
name a different operation.
|
|
147
|
+
- A compiled word list keeps the `VaryingString` objects of `CENSOR_WORDSET` alive until the
|
|
148
|
+
list changes or the `Profanity` object is deleted. An attribute added to one of them that
|
|
149
|
+
refers back to the `Profanity` object keeps that object alive.
|
|
150
|
+
- Free-threaded CPython builds are not supported: the extension module asks to keep the GIL
|
|
151
|
+
enabled.
|
|
152
|
+
- `__version__` and `name` are those of `better_profanity_rs`.
|
|
153
|
+
|
|
154
|
+
## Development
|
|
155
|
+
|
|
156
|
+
```sh
|
|
157
|
+
cargo test -p better-profanity
|
|
158
|
+
cd bindings/python
|
|
159
|
+
uv sync --group dev
|
|
160
|
+
uv run pytest
|
|
161
|
+
cd benchmarks && uv run --group bench python parity.py # all corpora
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## License
|
|
165
|
+
|
|
166
|
+
MIT, the same licence as `better_profanity`, whose code, word list and character list this
|
|
167
|
+
project includes. See [NOTICE](NOTICE).
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
[package]
|
|
2
|
+
name = "better-profanity-python"
|
|
3
|
+
description = "Python bindings for better-profanity"
|
|
4
|
+
publish = false
|
|
5
|
+
version.workspace = true
|
|
6
|
+
authors.workspace = true
|
|
7
|
+
edition.workspace = true
|
|
8
|
+
rust-version.workspace = true
|
|
9
|
+
license.workspace = true
|
|
10
|
+
repository.workspace = true
|
|
11
|
+
readme = "README.md"
|
|
12
|
+
|
|
13
|
+
[lib]
|
|
14
|
+
name = "_core"
|
|
15
|
+
crate-type = ["cdylib"]
|
|
16
|
+
|
|
17
|
+
[dependencies]
|
|
18
|
+
pyo3 = { version = "0.29", features = ["abi3-py39"] }
|
|
19
|
+
better-profanity = { path = "../../crates/better-profanity" }
|
|
20
|
+
rustc-hash = "2"
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Alex Kholodniak
|
|
4
|
+
Copyright (c) 2018 The Python Packaging Authority
|
|
5
|
+
|
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
8
|
+
in the Software without restriction, including without limitation the rights
|
|
9
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
10
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
11
|
+
furnished to do so, subject to the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be included in all
|
|
14
|
+
copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
19
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
21
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
22
|
+
SOFTWARE.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
better-profanity-rs
|
|
2
|
+
Copyright 2026 Alex Kholodniak
|
|
3
|
+
|
|
4
|
+
This product is a port of better_profanity (https://github.com/snguyenthanh/better_profanity)
|
|
5
|
+
by Son Nguyen Thanh, licensed under the MIT License. Its LICENSE file reads
|
|
6
|
+
"Copyright (c) 2018 The Python Packaging Authority"; that notice is kept in LICENSE.
|
|
7
|
+
|
|
8
|
+
The following files are copies of better_profanity 0.7.0, unchanged:
|
|
9
|
+
|
|
10
|
+
- crates/better-profanity/data/profanity_wordlist.txt and
|
|
11
|
+
bindings/python/python/better_profanity_rs/profanity_wordlist.txt (the word list);
|
|
12
|
+
- crates/better-profanity/data/alphabetic_unicode.json and
|
|
13
|
+
bindings/python/python/better_profanity_rs/alphabetic_unicode.json;
|
|
14
|
+
- bindings/python/python/better_profanity_rs/better_profanity.py, constants.py, utils.py and
|
|
15
|
+
varying_string.py, which the Python package runs for inputs the Rust core does not handle.
|
|
16
|
+
|
|
17
|
+
bindings/python/python/better_profanity_rs/_original.py holds the source of
|
|
18
|
+
Profanity._hide_swear_words from better_profanity 0.7.0 as a string, unchanged.
|