persian-text-guard 1.4.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,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Amirehsan Kohannasab
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, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,454 @@
1
+ Metadata-Version: 2.5
2
+ Name: persian-text-guard
3
+ Version: 1.4.0
4
+ Summary: Persian text normalization and evasion-resistant profanity filtering for Python. Folds Arabic/Persian look-alike letters, zero-width and bidi characters, and all three digit ranges, and catches the ways people get around a word list: spaced or dotted letters, held keys, leetspeak, filler symbols, Cyrillic look-alikes and Finglish. Finds every match with its position and censors messages with a fixed mask. Ships an optional curated Persian/Finglish/English word list of about 1,250 categorised entries.
5
+ Project-URL: Homepage, https://github.com/AmirehsanK/PersianTextGuard/tree/main/python#readme
6
+ Project-URL: Source, https://github.com/AmirehsanK/PersianTextGuard
7
+ Project-URL: Issues, https://github.com/AmirehsanK/PersianTextGuard/issues
8
+ Author: Amirehsan Kohannasab
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ License-File: THIRD-PARTY-NOTICES.md
12
+ Keywords: arabic,farsi,finglish,moderation,normalization,persian,profanity,profanity-filter,python,rtl,text
13
+ Classifier: Development Status :: 5 - Production/Stable
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Natural Language :: English
16
+ Classifier: Natural Language :: Persian
17
+ Classifier: Operating System :: OS Independent
18
+ Classifier: Programming Language :: Python :: 3 :: Only
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Programming Language :: Python :: 3.14
23
+ Classifier: Programming Language :: Python :: Free Threading :: 3 - Stable
24
+ Classifier: Topic :: Text Processing :: Linguistic
25
+ Classifier: Typing :: Typed
26
+ Requires-Python: >=3.11
27
+ Description-Content-Type: text/markdown
28
+
29
+ # persian-text-guard
30
+
31
+ Persian text normalization and evasion-resistant profanity filtering for Python.
32
+
33
+ [فارسی](#فارسی) · [Full project README](https://github.com/AmirehsanK/PersianTextGuard#readme) · [.NET package](https://www.nuget.org/packages/PersianTextGuard) · [npm package](https://www.npmjs.com/package/persian-text-guard)
34
+
35
+ A word list is easy to get around. Type an Arabic `ي` instead of `ی`, slip a zero-width character
36
+ inside a word, write `f u c k`, `sh1t`, `کیییییر` or `k0s`, and a plain `word in text` check sees
37
+ nothing. `persian-text-guard` reads through those tricks, and just as importantly, it doesn't flag
38
+ ordinary messages: `هر کس`, `تخم مرغ`, `class`, `push it` and `Scunthorpe` all pass.
39
+
40
+ It is the Python port of [PersianTextGuard](https://github.com/AmirehsanK/PersianTextGuard), and gives
41
+ the same answers as the .NET and JavaScript packages for every case in the shared
42
+ [conformance corpus](https://github.com/AmirehsanK/PersianTextGuard/tree/main/conformance).
43
+
44
+ - Pure Python, with full type hints (`py.typed`), for CPython 3.11 and later, free-threaded 3.14
45
+ included.
46
+ - No dependencies.
47
+ - Safe to share across threads.
48
+
49
+ ## Installation
50
+
51
+ ```bash
52
+ pip install persian-text-guard
53
+ ```
54
+
55
+ or, in a project managed by uv:
56
+
57
+ ```bash
58
+ uv add persian-text-guard
59
+ ```
60
+
61
+ ## Quick start
62
+
63
+ ```python
64
+ from persian_text_guard import ProfanityFilter, WordList
65
+
66
+ # Build once and share it: a filter never changes after it is built.
67
+ filter = ProfanityFilter(WordList.persian_default())
68
+
69
+ assert not filter.contains_profanity("سلام، سفارشم کی میرسه؟")
70
+ assert filter.contains_profanity("ک.ی.ر")
71
+ assert filter.contains_profanity("f u c k")
72
+ ```
73
+
74
+ ### What matched, and why
75
+
76
+ ```python
77
+ from persian_text_guard import EvasionKind, ProfanityFilter, WordCategory, WordList
78
+
79
+ filter = ProfanityFilter(WordList.persian_default())
80
+ match = filter.find_match("sh1iiit")
81
+
82
+ assert match is not None
83
+ assert match.word.text == "shit"
84
+ assert match.word.category == WordCategory.PROFANITY
85
+ assert match.evasion == (EvasionKind.REPEATED_LETTERS, EvasionKind.LOOKALIKE_CHARACTERS)
86
+ assert filter.find_match("hello") is None
87
+ ```
88
+
89
+ ### Every match, with its position
90
+
91
+ Positions are Python string indexes (code points), so slicing gives the matched text:
92
+
93
+ ```python
94
+ from persian_text_guard import ProfanityFilter, WordList
95
+
96
+ filter = ProfanityFilter(WordList.persian_default())
97
+ message = "sh1t and f u c k"
98
+
99
+ found = [
100
+ (match.word.text, match.index, match.length, message[match.index : match.index + match.length])
101
+ for match in filter.find_matches(message)
102
+ ]
103
+ assert found == [("shit", 0, 4, "sh1t"), ("fuck", 9, 7, "f u c k")]
104
+ ```
105
+
106
+ This holds for emoji and every other character outside the Basic Multilingual Plane too: each counts
107
+ as one position, as it does everywhere else in Python.
108
+
109
+ ```python
110
+ from persian_text_guard import ProfanityFilter, WordList
111
+
112
+ filter = ProfanityFilter(WordList.persian_default())
113
+ message = "\N{GRINNING FACE} کیر"
114
+
115
+ match = filter.find_match(message)
116
+ assert match is not None
117
+ assert (match.index, match.length) == (2, 3)
118
+ assert message[match.index : match.index + match.length] == "کیر"
119
+ ```
120
+
121
+ ### Censoring
122
+
123
+ ```python
124
+ from persian_text_guard import ProfanityFilter, WordList
125
+
126
+ filter = ProfanityFilter(WordList.persian_default())
127
+
128
+ assert filter.censor("kir and motherfucker") == "**** and ****"
129
+ assert filter.censor("جنده\N{ZERO WIDTH NON-JOINER}ها رو ببین") == "**** رو ببین"
130
+ assert filter.censor("this is kir", "#") == "this is ####"
131
+ assert filter.censor("hello") == "hello"
132
+ ```
133
+
134
+ Whole words are hidden, every mask is four characters, and the output is always clean. Clean text
135
+ comes back unchanged, and everything outside a hidden word is returned exactly as given. The mask can
136
+ be any symbol or punctuation; a letter, digit, whitespace or control character raises `ValueError`.
137
+
138
+ ### Categories
139
+
140
+ Every bundled entry has a category: `profanity`, `sexual`, `insult`, `slur`, `harassment` or `mild`.
141
+
142
+ ```python
143
+ from persian_text_guard import ProfanityFilter, WordCategory, WordList
144
+
145
+ everyday = ProfanityFilter(WordList.persian_default()) # everything except mild
146
+ strict = ProfanityFilter(WordList.all()) # mild too
147
+ slurs_and_abuse = ProfanityFilter(WordList.bundled(WordCategory.SLUR, "harassment"))
148
+
149
+ assert not everyday.contains_profanity("این فیلم آشغال بود")
150
+ assert strict.contains_profanity("این فیلم آشغال بود")
151
+ assert slurs_and_abuse.contains_profanity("kys")
152
+ assert not slurs_and_abuse.contains_profanity("کیر")
153
+ ```
154
+
155
+ A category, like a match mode or a normalization step, can be given as an enum member or as its name
156
+ (`"slur"`); a type checker rejects a name that does not exist, and so does the code at run time.
157
+
158
+ ### Your own words
159
+
160
+ ```python
161
+ from persian_text_guard import BannedWord, ProfanityFilter, WordList, WordMatchMode
162
+
163
+ filter = ProfanityFilter(
164
+ [
165
+ BannedWord("اسپم"), # whole word (the default)
166
+ BannedWord("casino", WordMatchMode.ANYWHERE), # inside longer words too
167
+ ]
168
+ )
169
+ assert filter.contains_profanity("این پیام اسپم است")
170
+ assert filter.contains_profanity("onlinecasino.example")
171
+
172
+ # Or combine your entries with the bundled list:
173
+ combined = ProfanityFilter([*WordList.persian_default(), BannedWord("casino", "anywhere", "mild")])
174
+ assert combined.contains_profanity("casino night")
175
+ ```
176
+
177
+ ### Word-list files
178
+
179
+ Word lists can also be kept in a text file, one entry per line. `WordList.load` reads a path or an open
180
+ file as UTF-8, and `WordList.parse` reads text you already have:
181
+
182
+ ```python
183
+ import tempfile
184
+ from pathlib import Path
185
+
186
+ from persian_text_guard import ProfanityFilter, WordCategory, WordList, WordMatchMode
187
+
188
+ text = "# comments start with #\n[insult]\nidiot\n~scam\n"
189
+
190
+ words = WordList.parse(text)
191
+ assert [(w.text, w.mode, w.category) for w in words] == [
192
+ ("idiot", WordMatchMode.WHOLE_WORD, WordCategory.INSULT),
193
+ ("scam", WordMatchMode.ANYWHERE, WordCategory.INSULT),
194
+ ]
195
+ assert ProfanityFilter(words).contains_profanity("scammers everywhere")
196
+
197
+ with tempfile.TemporaryDirectory() as folder:
198
+ path = Path(folder) / "words.txt"
199
+ path.write_text(text, encoding="utf-8")
200
+ assert WordList.load(path) == words
201
+ with path.open("rb") as file:
202
+ assert WordList.load(file) == words # an open file is read, and left open
203
+ ```
204
+
205
+ A heading such as `[insult]` names a category in any letter case. An unknown heading raises a
206
+ `WordListFormatError`, a `ValueError` with the heading's `line`. A UTF-8 byte order mark is ignored.
207
+
208
+ ### Options
209
+
210
+ Every evasion the filter reads through can be turned off:
211
+
212
+ ```python
213
+ from persian_text_guard import ProfanityFilter, ProfanityFilterOptions, WordList
214
+
215
+ literal = ProfanityFilter(WordList.persian_default(), ProfanityFilterOptions(join_spaced_letters=False))
216
+
217
+ assert not literal.contains_profanity("f u c k")
218
+ assert literal.contains_profanity("fuuuck")
219
+ ```
220
+
221
+ ### Normalization
222
+
223
+ ```python
224
+ from persian_text_guard import NormalizationStep, normalize, to_ascii_digits, to_persian_digits, tokenize
225
+
226
+ text = "كتاب\N{ZERO WIDTH NON-JOINER}هاي ۱۲ ABC"
227
+
228
+ # "comparison" (the default): everything folded, for searching and matching.
229
+ assert normalize(text) == "کتابهای 12 abc"
230
+ # "standard": safe to store and show; keeps the zero-width non-joiner, Persian digits and case.
231
+ assert normalize(text, "standard") == "کتاب\N{ZERO WIDTH NON-JOINER}های ۱۲ ABC"
232
+ # Or any combination of steps, as members or names.
233
+ assert normalize("ABC ي", [NormalizationStep.LOWER_CASE, "unifyLetters"]) == "abc ی"
234
+
235
+ assert tokenize("سلام، دنیا! خوبی؟") == ["سلام", "دنیا", "خوبی"]
236
+ assert to_persian_digits("2 ساعت پیش") == "۲ ساعت پیش"
237
+ assert to_ascii_digits("۱۴۰۴/۰۵/۱۴") == "1404/05/14"
238
+ ```
239
+
240
+ ## Thread safety
241
+
242
+ A filter is immutable after it is built, and the bundled lists are parsed once, under a lock. Build one
243
+ filter and share it between threads, including on free-threaded CPython 3.14 with the GIL disabled:
244
+
245
+ ```python
246
+ from concurrent.futures import ThreadPoolExecutor
247
+
248
+ from persian_text_guard import ProfanityFilter, WordList
249
+
250
+ filter = ProfanityFilter(WordList.persian_default())
251
+ messages = ["سلام", "ک.ی.ر", "f u c k", "hello"] * 50
252
+
253
+ with ThreadPoolExecutor(max_workers=8) as pool:
254
+ results = list(pool.map(filter.contains_profanity, messages))
255
+
256
+ assert results == [filter.contains_profanity(message) for message in messages]
257
+ ```
258
+
259
+ ## Validating input
260
+
261
+ Checking, finding matches, censoring, normalizing and tokenizing accept any `str`, including empty
262
+ text, broken emoji, lone surrogates and very long messages, and treat `None` as an empty message. They
263
+ never raise for text.
264
+
265
+ Anything else, such as `bytes`, a number or a list, raises `TypeError`, so a missing check shows up at
266
+ once instead of an unchecked value passing as clean. Values from a request are untrusted, so check that
267
+ the message is a string first:
268
+
269
+ ```python
270
+ import json
271
+
272
+ from persian_text_guard import ProfanityFilter, WordList
273
+
274
+ filter = ProfanityFilter(WordList.persian_default())
275
+ body = json.loads('{"message": ["kir"]}')
276
+
277
+ message = body.get("message")
278
+ if not isinstance(message, str):
279
+ outcome = "rejected: message must be text"
280
+ else:
281
+ outcome = filter.censor(message)
282
+ assert outcome == "rejected: message must be text"
283
+ ```
284
+
285
+ | Error | Raised by | When |
286
+ | --- | --- | --- |
287
+ | `TypeError` | every text function, `censor`, `WordList.parse`, the constructors | A value that is not a `str` (or `None`, where `None` is allowed), an option that is not a `bool`, or an entry that is not a `BannedWord` |
288
+ | `ValueError` | `censor`, enum parameters, `normalize` | An invalid mask, or an unknown category, mode, step or preset name |
289
+ | `WordListFormatError` (a `ValueError`) | `WordList.parse`, `WordList.load` | An unknown section heading; `.line` is 1-based |
290
+ | `OSError`, `UnicodeDecodeError` | `WordList.load` | The file cannot be read, or is not UTF-8 |
291
+
292
+ ## .NET, JavaScript and Python
293
+
294
+ | .NET | JavaScript/TypeScript | Python |
295
+ | --- | --- | --- |
296
+ | `new ProfanityFilter(words, options)` | `new ProfanityFilter(words, options)` | `ProfanityFilter(words, options)` |
297
+ | `filter.Count` | `filter.count` | `filter.count` |
298
+ | `filter.ContainsProfanity(text)` | `filter.containsProfanity(text)` | `filter.contains_profanity(text)` |
299
+ | `filter.FindMatch(text)` | `filter.findMatch(text)` | `filter.find_match(text)` (`None` when there is no match) |
300
+ | `filter.FindMatches(text)` | `filter.findMatches(text)` | `filter.find_matches(text)` (a tuple) |
301
+ | `filter.Censor(text, '#')` | `filter.censor(text, '#')` | `filter.censor(text, "#")` |
302
+ | `new BannedWord("x", WordMatchMode.Anywhere) { Category = WordCategory.Slur }` | `{ text: 'x', mode: 'anywhere', category: 'slur' }` | `BannedWord("x", WordMatchMode.ANYWHERE, WordCategory.SLUR)` |
303
+ | `ProfanityFilterOptions { SqueezeRepeatedLetters = false }` | `{ squeezeRepeatedLetters: false }` | `ProfanityFilterOptions(squeeze_repeated_letters=False)` |
304
+ | `match.Word`, `match.Evasion`, `match.Index`, `match.Length` | `match.word`, `.evasion`, `.index`, `.length` | `match.word`, `.evasion`, `.index`, `.length` (code points) |
305
+ | `EvasionKind.LookalikeCharacters \| EvasionKind.RepeatedLetters` | `['repeatedLetters', 'lookalikeCharacters']` | `(EvasionKind.REPEATED_LETTERS, EvasionKind.LOOKALIKE_CHARACTERS)` |
306
+ | `WordList.All`, `WordList.PersianDefault` | `WordList.all`, `WordList.persianDefault` | `WordList.all()`, `WordList.persian_default()` |
307
+ | `WordList.Bundled(WordCategory.Slur)` | `WordList.bundled('slur')` | `WordList.bundled(WordCategory.SLUR)` or `WordList.bundled("slur")` |
308
+ | `WordList.Parse(text)` | `WordList.parse(text)` | `WordList.parse(text)` |
309
+ | `WordList.Load(stream)` | read the file, then `WordList.parse(text)` | `WordList.load(path_or_file)` |
310
+ | `PersianNormalizer.Normalize(text, PersianNormalization.Standard)` | `normalize(text, 'standard')` | `normalize(text, "standard")` |
311
+ | `PersianNormalizer.Tokenize(text)` | `tokenize(text)` | `tokenize(text)` |
312
+ | `PersianNormalizer.ToPersianDigits(text)` | `toPersianDigits(text)` | `to_persian_digits(text)` |
313
+ | `FormatException` | `WordListFormatError` (`.line`) | `WordListFormatError` (`.line`), a `ValueError` |
314
+ | `ArgumentException` (mask) | `RangeError` | `ValueError` |
315
+
316
+ ## Performance
317
+
318
+ Against `WordList.persian_default()` (about 1,000 entries), on CPython 3.14.6 (Intel Core i7-9700K,
319
+ pyperf with `--rigorous`):
320
+
321
+ | Operation | Mean | Operations/s |
322
+ | --- | ---: | ---: |
323
+ | Short clean message (5 words) | 91.4 µs | 10,937 |
324
+ | Long clean message (60 words) | 909 µs | 1,100 |
325
+ | Message with evasions | 50.4 µs | 19,850 |
326
+ | Normalize a long message | 35.1 µs | 28,454 |
327
+ | Build a filter from the bundled list | 13.3 ms | 75 |
328
+ | `find_matches`, clean short message | 90.4 µs | 11,059 |
329
+ | `find_matches`, message with three banned words | 240 µs | 4,169 |
330
+ | `censor`, short message with one banned word | 118 µs | 8,461 |
331
+ | `censor`, 60-word message with three banned words | 3.2 ms | 311 |
332
+ | A 132,000-character message | 210 ms | 5 |
333
+
334
+ Build the filter once and reuse it. The messages are the same as the .NET and JavaScript benchmarks';
335
+ run them with `uv run python bench/bench_filter.py -o .bench/results.json` in `python/`, and print the
336
+ table with `uv run python scripts/bench_table.py .bench/results.json`.
337
+
338
+ ## Limitations
339
+
340
+ - **Unicode versions.** The filter uses the running Python's own Unicode data (`unicodedata`),
341
+ through a layer that reproduces .NET's rules for whitespace, case and categories. CPython 3.14 and
342
+ .NET 10 agree on the category of every code point. Older versions differ only on characters assigned
343
+ in Unicode 15 or later, and Python 3.11 also does not fold the 62 Cyrillic modifier letters
344
+ U+1E030–U+1E06D in compatibility normalization. The conformance corpus contains none of these
345
+ characters; if a difference matters to you, please open an issue.
346
+ - **Values that are not text.** `bytes`, numbers and other objects raise `TypeError` instead of being
347
+ read as an empty or converted message; see [Validating input](#validating-input).
348
+ - **Interpreters.** CPython 3.11 to 3.14, including free-threaded 3.14, is tested. Other interpreters,
349
+ such as PyPy, may work but are not tested.
350
+ - The evasions the filter reads through, and what it deliberately does not catch, are listed in the
351
+ [project README](https://github.com/AmirehsanK/PersianTextGuard#what-it-reads-through).
352
+
353
+ ## Links
354
+
355
+ - [Project README](https://github.com/AmirehsanK/PersianTextGuard#readme)
356
+ - [Conformance corpus](https://github.com/AmirehsanK/PersianTextGuard/tree/main/conformance)
357
+ - [Word lists](https://github.com/AmirehsanK/PersianTextGuard/tree/main/wordlists)
358
+ - [.NET package on NuGet](https://www.nuget.org/packages/PersianTextGuard) and
359
+ [JavaScript package on npm](https://www.npmjs.com/package/persian-text-guard)
360
+ - [MIT licence](https://github.com/AmirehsanK/PersianTextGuard/blob/main/LICENSE)
361
+
362
+ ---
363
+
364
+ <div dir="rtl">
365
+
366
+ ## فارسی
367
+
368
+ </div>
369
+
370
+ <div dir="rtl">
371
+
372
+ این بسته نسخهٔ پایتونِ پروژهٔ PersianTextGuard است: یکسان‌سازی متن فارسی و فیلتر کلمات نامناسب که ترفندهای دور زدن فهرست کلمات را می‌شناسد، مثل «ک.ی.ر»، حروف فاصله‌دار، حروف عربی به‌جای فارسی و نویسه‌های نامرئی، و در عین حال پیام‌های عادی مثل «هر کس» و «تخم مرغ» را رد نمی‌کند.
373
+
374
+ </div>
375
+
376
+ <div dir="rtl">
377
+
378
+ برای هر پیام همان پاسخی را می‌دهد که بسته‌های دات‌نت و جاوااسکریپت می‌دهند، و جایگاه‌ها را مثل خودِ پایتون می‌شمارد.
379
+
380
+ </div>
381
+
382
+ <div dir="rtl">
383
+
384
+ ### نصب
385
+
386
+ </div>
387
+
388
+ ```bash
389
+ pip install persian-text-guard
390
+ ```
391
+
392
+ <div dir="rtl">
393
+
394
+ ### شروع سریع
395
+
396
+ </div>
397
+
398
+ <div dir="rtl">
399
+
400
+ فیلتر را یک بار بسازید و همه‌جا از همان استفاده کنید؛ فیلتر بعد از ساخته شدن هرگز تغییر نمی‌کند و استفادهٔ هم‌زمان از چند نخ هم امن است.
401
+
402
+ </div>
403
+
404
+ ```python
405
+ from persian_text_guard import ProfanityFilter, WordList
406
+
407
+ # فیلتر را یک بار بسازید و به اشتراک بگذارید.
408
+ # Build once and share it: a filter never changes after it is built.
409
+ filter = ProfanityFilter(WordList.persian_default())
410
+
411
+ assert not filter.contains_profanity("سلام، سفارشم کی میرسه؟")
412
+ assert filter.contains_profanity("ک.ی.ر")
413
+ ```
414
+
415
+ <div dir="rtl">
416
+
417
+ ### سانسور
418
+
419
+ </div>
420
+
421
+ <div dir="rtl">
422
+
423
+ هر کلمهٔ نامناسب، با پسوندها و فاصله‌هایش، با چهار نویسهٔ ثابت پوشانده می‌شود و بقیهٔ متن دست نمی‌خورد.
424
+
425
+ </div>
426
+
427
+ ```python
428
+ from persian_text_guard import ProfanityFilter, WordList
429
+
430
+ # کلمات نامناسب با چهار نویسه پوشانده می‌شوند.
431
+ # Every banned word is hidden behind a four-character mask.
432
+ filter = ProfanityFilter(WordList.persian_default())
433
+
434
+ assert filter.censor("جنده\N{ZERO WIDTH NON-JOINER}ها رو ببین") == "**** رو ببین"
435
+ assert filter.censor("this is kir", "#") == "this is ####"
436
+ ```
437
+
438
+ <div dir="rtl">
439
+
440
+ ### بررسی ورودی
441
+
442
+ </div>
443
+
444
+ <div dir="rtl">
445
+
446
+ هر رشته‌ای پذیرفته می‌شود و مقدار خالیِ پایتون مثل پیام خالی است، اما هر مقدار دیگری، مثل عدد یا بایت یا فهرست، خطای نوع می‌دهد. پیش از فیلتر کردنِ داده‌های درخواست، بررسی کنید که پیام از نوع رشته باشد.
447
+
448
+ </div>
449
+
450
+ <div dir="rtl">
451
+
452
+ مستندات کامل، فهرست ترفندهایی که شناخته می‌شوند و محدودیت‌ها در [راهنمای اصلی پروژه](https://github.com/AmirehsanK/PersianTextGuard#readme) آمده است.
453
+
454
+ </div>