nya-codeblock 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,9 @@
1
+ root = true
2
+
3
+ [*]
4
+ charset = utf-8
5
+ end_of_line = lf
6
+ insert_final_newline = true
7
+ trim_trailing_whitespace = true
8
+ indent_style = tab
9
+ tab_width = 4
@@ -0,0 +1,10 @@
1
+ # Python-generated files
2
+ __pycache__/
3
+ *.py[oc]
4
+ build/
5
+ dist/
6
+ wheels/
7
+ *.egg-info
8
+
9
+ # Virtual environments
10
+ .venv
@@ -0,0 +1,3 @@
1
+ [submodule "pypi-uv-publish.sh"]
2
+ path = pypi-uv-publish.sh
3
+ url = git@github.com:anamoyee/pypi-uv-publish.sh
@@ -0,0 +1 @@
1
+ 3.11
@@ -0,0 +1,10 @@
1
+ Metadata-Version: 2.4
2
+ Name: nya-codeblock
3
+ Version: 0.1.0
4
+ Summary: Provide helpful functions to convert str into a markdown/discord codeblock. Ported from my own package, tcrutils.
5
+ Requires-Python: >=3.11
6
+ Description-Content-Type: text/markdown
7
+
8
+ # nya-codeblock
9
+
10
+ Provide helpful functions to convert str into a markdown/discord codeblock. Ported from my own package, tcrutils.
@@ -0,0 +1,3 @@
1
+ # nya-codeblock
2
+
3
+ Provide helpful functions to convert str into a markdown/discord codeblock. Ported from my own package, tcrutils.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 anamoyee
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,10 @@
1
+ # A git submodule for easy deployment of python packages using uv and GNU pass
2
+
3
+ Fork if your pass path differs
4
+
5
+ ## Usage
6
+ Once per repository:
7
+ `git submodule add https://github.com/anamoyee/pypi-uv-publish.sh`
8
+
9
+ Every time you want to publish to PyPI:
10
+ `./pypi-uv-publish.sh/publish.sh`
@@ -0,0 +1,24 @@
1
+ #!/usr/bin/env bash
2
+
3
+ set -euo pipefail
4
+ cd -- "$(dirname -- "${BASH_SOURCE[0]}")"
5
+ cd .. # meant to be used as a submodule that resides at root of the pyproject.
6
+
7
+ # build, then publish via uv
8
+
9
+ # assume PYPI token is stored in GNU password-store
10
+ # at /pypi_tokens/{TOKEN_NAME}
11
+
12
+ TOKEN_NAME="__main__"
13
+
14
+ if [[ TOKEN_NAME == "" ]]; then
15
+ echo "Provide a TOKEN_NAME in a form of a (GNU password-store) path" >&2
16
+ exit 1
17
+ fi
18
+
19
+ rm ./dist -rf # remove builds of old versions, dont upload more and more every time
20
+
21
+ uv build
22
+
23
+ UV_PUBLISH_USERNAME="__token__" UV_PUBLISH_PASSWORD="$(/usr/bin/env pass "pypi_tokens/${TOKEN_NAME}")" uv publish
24
+
@@ -0,0 +1,18 @@
1
+ [project]
2
+ name = 'nya-codeblock'
3
+ dynamic = ['version']
4
+ description = 'Provide helpful functions to convert str into a markdown/discord codeblock. Ported from my own package, tcrutils.'
5
+ readme = "README.md"
6
+ requires-python = ">=3.11"
7
+ dependencies = []
8
+
9
+ [build-system]
10
+ requires = ["hatchling"]
11
+ build-backend = "hatchling.build"
12
+
13
+ [tool.hatch.version]
14
+ path = "src/nya_codeblock/_version.py"
15
+ pattern = '__version__(?:: str)? *= *\"(?P<version>[^\"]+)\"'
16
+
17
+ [dependency-groups]
18
+ dev = ["pytest>=9.1.1"]
@@ -0,0 +1,606 @@
1
+ # Exclude a variety of commonly ignored directories.
2
+ exclude = [ #
3
+ ".bzr",
4
+ ".direnv",
5
+ ".eggs",
6
+ ".git",
7
+ ".git-rewrite",
8
+ ".hg",
9
+ ".ipynb_checkpoints",
10
+ ".mypy_cache",
11
+ ".nox",
12
+ ".pants.d",
13
+ ".pyenv",
14
+ ".pytest_cache",
15
+ ".pytype",
16
+ ".ruff_cache",
17
+ ".svn",
18
+ ".tox",
19
+ ".venv",
20
+ "venv",
21
+ ".vscode",
22
+ "__pypackages__",
23
+ "_build",
24
+ "buck-out",
25
+ "build",
26
+ "dist",
27
+ "node_modules",
28
+ "site-packages",
29
+ ]
30
+
31
+ line-length = 150
32
+ indent-width = 4
33
+
34
+ target-version = "py314"
35
+
36
+ [format]
37
+ quote-style = "double"
38
+ indent-style = "tab"
39
+
40
+ nested-string-quote-style = "preferred"
41
+
42
+ skip-magic-trailing-comma = false
43
+
44
+ line-ending = "auto"
45
+
46
+ docstring-code-format = true
47
+ docstring-code-line-length = "dynamic"
48
+
49
+ [lint]
50
+ # Allow select greek letters
51
+ allowed-confusables = ["α", "β", "γ", "δ", "ε", "ζ", "η", "θ", "κ", "λ", "μ", "ξ", "π", "σ", "ς", "τ", "φ", "ψ", "ω", "Γ", "Δ", "Π", "Σ", "Ω"]
52
+
53
+ preview = true
54
+
55
+ pydocstyle.convention = "google"
56
+ # (using: )
57
+ #
58
+ # Args:
59
+ # whatever
60
+ #
61
+ # (instead of: )
62
+ #
63
+ # Args
64
+ # ----
65
+ # whatever
66
+
67
+ flake8-annotations.suppress-dummy-args = true
68
+ flake8-annotations.suppress-none-returning = true
69
+
70
+ extend-per-file-ignores = { "tests/**/*.py" = ["ANN"], "test*.py" = ["ANN"] } # don't require annotations for tests
71
+
72
+ select = [
73
+ # commented-out code
74
+ #"ERA001",
75
+
76
+ # FastAPI
77
+ "FAST",
78
+
79
+ # flake8-2020
80
+ "YTT101",
81
+ "YTT102",
82
+ "YTT103",
83
+
84
+ "YTT201",
85
+ "YTT202",
86
+ "YTT203",
87
+ "YTT204",
88
+
89
+ "YTT301", # "python 10.0" :sob:
90
+ "YTT302",
91
+ "YTT303",
92
+
93
+ # flake8-annotations
94
+ "ANN001",
95
+ "ANN002",
96
+ "ANN003",
97
+ "ANN201",
98
+ "ANN202",
99
+ #"ANN204", # obvious methods shouldn't be annotated (think: __init__ -> None, __repr__ -> str)
100
+ "ANN205",
101
+ "ANN206",
102
+
103
+ # flake8-async
104
+ "ASYNC",
105
+
106
+ # flake8-bandit
107
+ "S113", # request without a timeout
108
+
109
+ # flake8 boolean fly trap
110
+ "FBT001", # def f(b: bool): ... (should be (*, b: ...) )
111
+ "FBT002", # def f(b=False): ... (should be (*, b=...) )
112
+ # Specifically not opting in for the caller side (FBT003), only definition side. This could prevent integration with badly written libraries...
113
+
114
+ # flake8-bugbear
115
+ "B002",
116
+ "B003",
117
+ "B004",
118
+ "B005",
119
+ "B007",
120
+ "B009",
121
+ "B010",
122
+ "B011",
123
+ "B012",
124
+ "B013",
125
+ "B014",
126
+ "B015",
127
+ "B016",
128
+ "B018",
129
+ "B019",
130
+ "B020",
131
+ "B021",
132
+ "B022",
133
+ "B023",
134
+ "B024",
135
+ "B025",
136
+ "B026",
137
+ "B027",
138
+ "B028",
139
+ "B029",
140
+ "B030",
141
+ "B031",
142
+ "B033",
143
+ "B034",
144
+ "B035",
145
+ "B039",
146
+ "B043",
147
+ "B901",
148
+ "B903",
149
+ "B904",
150
+ "B905",
151
+ "B909",
152
+ "B911",
153
+ "B912",
154
+
155
+ # flake8-builtins
156
+ "A001",
157
+ "A002",
158
+ "A003",
159
+ "A004",
160
+ "A005",
161
+ # explicitly opting out of A006, since lambda argument shadowing seems a bit too strict..
162
+
163
+ # flake8-commas
164
+ #"COM812", trailing comma - should be handled by the magic comma ruff formatter or whatever
165
+ "COM818",
166
+ "COM819",
167
+
168
+ # flake8-comprehensions
169
+ "C400",
170
+ "C401",
171
+ "C402",
172
+ "C403",
173
+ "C404",
174
+ "C405",
175
+ "C406",
176
+ # specifically opting out of C408, since doing dict(x=1, y=2) is a common pattern that i like to use sometimes
177
+ "C409",
178
+ "C410",
179
+ "C411",
180
+ "C413",
181
+ "C414",
182
+ "C415",
183
+ "C416",
184
+ "C417",
185
+ "C418", # owo
186
+ "C419",
187
+ "C420",
188
+
189
+ # flake8-datetimez
190
+ "DTZ001",
191
+ "DTZ002",
192
+ "DTZ003",
193
+ "DTZ004",
194
+ "DTZ005",
195
+ "DTZ006",
196
+ "DTZ007",
197
+ "DTZ011",
198
+ "DTZ012",
199
+ "DTZ901",
200
+
201
+ # flake8-debugger
202
+ "T100",
203
+
204
+ # flake8-django
205
+ "DJ", # selecting entire category, since i dont know much about django and any help would be nice!
206
+
207
+ # flake8-errmsg
208
+ "EM101", # Note for self: instead of removing those rules try to actually follow the `msg = ...; raise E(msg)` pattern.
209
+ "EM102",
210
+ "EM103",
211
+
212
+ # flake8-executable
213
+ "EXE001",
214
+ "EXE002",
215
+ "EXE003",
216
+ "EXE004",
217
+ "EXE005",
218
+
219
+ # flake8-fixme
220
+ "FIX001", # 'FIXME'
221
+ "FIX002", # 'TODO'
222
+ "FIX004", # 'HACK'
223
+
224
+ # flake8-gettext
225
+ "INT001",
226
+ "INT002",
227
+ "INT003",
228
+
229
+ # flake8-implicit-str-concat
230
+ "ISC001",
231
+ "ISC002",
232
+ "ISC003",
233
+ "ISC004",
234
+
235
+ # flake8-import-conventions
236
+ "ICN001",
237
+ "ICN002",
238
+ "ICN003",
239
+
240
+ # flake8-logging & flake8-logging-format
241
+ "LOG", # Just get all the logging rules, though i'm not so sure about using the default camelCase logging module...
242
+ "G",
243
+
244
+ # flake8-no-pep420 (forbid namespace packages)
245
+ "INP001",
246
+
247
+ # flake8-pie
248
+ "PIE790",
249
+ "PIE794",
250
+ "PIE796",
251
+ "PIE800",
252
+ "PIE804",
253
+ "PIE807",
254
+ "PIE808",
255
+ "PIE810",
256
+
257
+ # flake8-pyi
258
+ "PYI", # we'll see if this holds, because if i open up somebody else's .pyi and ruff starts yelling at me, i'm tossing this shit the hell out
259
+
260
+ # flake8-pytest-style
261
+ "PT",
262
+
263
+ # flake8-return
264
+ "RET",
265
+
266
+ # flake8-simplify
267
+ "SIM",
268
+
269
+ # flake8-slots
270
+ "SLOT",
271
+
272
+ # flake8-type-checking
273
+ "TC",
274
+
275
+ # flake8-unused-arguments
276
+ "ARG005", # opt in only for lambdas
277
+
278
+ # flake8-use-pathlib
279
+ "PTH",
280
+
281
+ # flynt
282
+ "FLY002",
283
+
284
+ # isort
285
+ "I001",
286
+
287
+ # numpy
288
+ "NPY",
289
+
290
+ # pandas
291
+ "PD",
292
+
293
+ # pep8-naming
294
+ "N804", # method first argument should be `self`
295
+ "N805", # classmethod first argument should be `cls`
296
+ "N811", # constant imported as non-constant-looking name
297
+ "N818", # 'Error' suffix on exception typedefs
298
+
299
+ # perflint
300
+ "PERF101",
301
+ "PERF102",
302
+
303
+ "PERF401",
304
+ "PERF402",
305
+ "PERF403",
306
+
307
+ # pycodestyle (error)
308
+ "E101", # mixed spaces and tabs
309
+ "E711", # none comparison using `==`, should be using `is`
310
+ "E712", # comparison `== True` or `== False`
311
+ "E713", # 'not x in y' -> 'x not in y'
312
+ "E714", # 'not x is y' -> 'x is not y'
313
+ "E722", # bare 'except:'
314
+ "E731", # do not assign a lambda expression, use a def
315
+ "E902", # IOError (ruff itself cannot read a file)
316
+
317
+ # pycodestyle (warning)
318
+ "W605", # invalid escape sequence
319
+
320
+ # pydoclint
321
+ "DOC",
322
+
323
+ # pydocstyle
324
+ # "D1", # 'thing X should be documented'
325
+ # "D417", # 'function/method param X should be documented within the already documented function.'
326
+
327
+ "D200", # docstring could be on a single line
328
+ "D201", # 'unwanted blank between def and """', specifically opting out of 'blank between def's """ and later code', since this blurs the text together.
329
+ "D207", # docstring under-indented
330
+ "D208", # docstring over-indented
331
+ "D204", # 'missing blank between class's """ and class body'
332
+ "D209", # multiline docstring should have their ending quotes on a new line
333
+ "D210", # unwanted whitespace around docstring text. """ This. """ should be """This."""
334
+ "D211", # 'unwanted blank between class and """'
335
+ "D212", # multiline docstring's first line should be on the same line as """
336
+ "D214", # docstring section over-indented
337
+ "D300", # docstring str should be using " quotes (as opposed to ' quotes)
338
+ "D301", # use r""" if any backslashes in docstring
339
+ "D400", # docstring should end in a period
340
+ "D403", # docstring first word should be capitalized
341
+ "D405", # docstring section name not properly capitalized (args -> Args)
342
+ "D412", # unwanted newline between docstring section and the section's content
343
+ "D416", # missing section names' ":", (Args -> Args:)
344
+ "D418", # overloads should not be documented separately
345
+ "D419", # docstring content missing entirely. ("""""")
346
+ "D420", # docstring sections in correct order
347
+
348
+ # pyflakes
349
+ "F401", # unused import
350
+ "F50", # various helpful %-format rules
351
+ "F52", # various helpful str.format() rules
352
+ "F541", # f-string without any placeholders
353
+ "F60", # in dictionary literal: key repeated
354
+ "F622", # `a, *b, c, *d, e = f` is invalid syntax (notice the two '*'s)
355
+ "F631", # `assert (...,)` where tuple has at least one element is always true
356
+ "F634", # same as above but for `if (...,)`
357
+ "F632", # use == when comparing to literals that arent None, True, False, etc.
358
+ "F707", # order of exception handlers in try-except is wrong (most general handler should be last)
359
+ "F811", # redefinition of unused name from line X
360
+ "F822", # undefined name in __all__
361
+ "F842", # unused annotation (`foo: int`)
362
+ "F901", # raise NotImplemented should be raise NotImplementedError
363
+
364
+ # pygrep-hooks
365
+ # "PGH003", # blanket `type: ignore` (a specific `type: ignore[whatever]` should be provided)
366
+ # "PGH004", # blanket `# noqa` (a specific `noqa: whatever` should be provided)
367
+ "PGH005", # blanket mock attribute access, where it should be a function call (e.g. `mock.assert_called` should be `mock.assert_called()`)
368
+
369
+ # pylint
370
+ "PLC0105", # typevar variance should be reflected in the name (T_co, U_contra)
371
+ "PLC0131", # typevar cannot be covariant and contravariant at the same time
372
+ "PLC0132", # typevar's bound name should be the same as declared name (`T = TypeVar("T")`, not `T = TypeVar("U")`)
373
+ "PLC0205", # __slots__ should be a tuple, not `__slots__ = "asdf"` (instead: `__slots__ = ("asdf",)`)
374
+ "PLC0206", # iteration over dict where it could be `.items()`
375
+ "PLC0207", # in str.split()[int], a maxsplit should be used, since it gets getitemed anyway
376
+ "PLC0208", # iteration over set literal, where tuple literal is faster to iterate over.
377
+ "PLC1802", # do not use `if len(x):`, use `if x:` instead
378
+ "PLC1901", # a variable of type `str` should not be `== ""`, use `not x` instead
379
+
380
+ "PLE010", # do not use `yield` or `return` in `__init__`
381
+ "PLE0115", # use of `nonlocal` and `global` at the same time on the same identifier
382
+ "PLE0116", # `continue` in `finally`
383
+ "PLE0117", # `nonlocal` used, but the nonlocal scope doesnt define that variable
384
+ "PLE0118", # `global` keyword should be used at the top of the scope
385
+ "PLE0237", # assignment outside of defined `__slots__`
386
+ "PLE0241", # duplicate base in class definition
387
+ "PLE030", # unexpected signature or return types of a magic methods (e.g. `def __len__(self, wtf): ...`)
388
+ "PLE060", # __all__ invalid
389
+ "PLE0643", # literal expression likely to raise IndexError (e.g. `[1, 2, 3][3]`)
390
+ "PLE0704", # misplaced bare `raise`
391
+ "PLE1141", # unpacking dict to pairs without calling .items()
392
+ "PLE12", # logging: too many or too few args
393
+ "PLE13", # invalid string formatting (e.g. '%z' % 'a' - invalid format specifier 'z'), and others
394
+ "PLE1519", # on methods use `@singledispatchmethod` instead of `@singledispatch`
395
+ "PLE1520", # same as above but in reverse for non-methods
396
+ "PLE2502", # check for unicode RTL in source code (could slip backdoors in, if not careful, since would look like a comment)
397
+ "PLE251", # actual control characters in source code, use escaped versions
398
+ "PLE4703", # set modified during iteration will cause a RuntimeError
399
+
400
+ "PLR0124", # x==x, convert to True or math.isnan(x)
401
+ "PLR0133", # 1==1, convert to True
402
+ "PLR0202", # manual classmethod decorator
403
+ "PLR0203", # manual staticmethod decorator
404
+ "PLR0206", # @property cannot have parameters
405
+ "PLR0402", # use from imports when needed
406
+ "PLR1708", # don't raise `StopIteration` in a generator, use `return` instead
407
+ "PLR1712", # unnecessary tmp variable in swap, use `a, b = b, a`
408
+ "PLR1714", # repeated == operator on same object, use the `in` operator
409
+ "PLR1716", # use the chained comparison (`a < b < c`) instead of `a < b and b < c`
410
+ "PLR5501", # replace else: if with elif
411
+ "PLR6104", # prefer += operator over x=x+1, and so on for other supported inplace operators
412
+ "PLR6201", # prefer sets for membership testing of literals
413
+ #"PLR6301", # refactor methods that don't use the `self` argument into functions or staticmethods
414
+
415
+ "PLW0108", # `lambda: str()` -> `str`
416
+ "PLW0120", # useless else on a loop
417
+ "PLW0127", # avoid useless x=x
418
+ "PLW0128", # avoid mentioning same itentifier in assignment unpacking left side, because it overrides
419
+ "PLW0129", # `assert "str"` is always true for nonempty strs
420
+ "PLW0131", # walrus operator unneeded
421
+ "PLW0177", # == float('nan')
422
+ "PLW0244", # __slots__ should only mention additional slots in subclass
423
+ "PLW0245", # super needs parenthesis to work
424
+ "PLW0406", # module imports itself, which causes a circular import error
425
+ "PLW0602", # global used, but this var only read, not written to
426
+ "PLW0604", # global at module scope is redundant
427
+ "PLW0711", # `except A or B` should be `except (A, B)` in 99.9% of cases (is a brainfart and not intentional)
428
+ "PLW1501", # bad `mode` string for open()
429
+ "PLW1507", # correct way to copy os.environ
430
+ "PLW1508", # .get() for env should have a valid default type (env vars always store strings)
431
+ "PLW1509", # subproces.Popen has built-in methods to not have to use a preexec_fn
432
+ "PLW1510", # subprocess.run should be used with `check=...` like `zip` with `strict=...` to avoid silent failures unless explicitly requested
433
+ "PLW1514", # always specify encoding
434
+ "PLW2101", # threading.Lock should share the same instance across threads, otherwise it's useless
435
+ #"PLW2901", # `for i in x: for i in y:` doesnt let you access the first loop's i, this also applies to multiple with statements; NOTE: disabled becase it also broke shit like for expr in exprs: expr = expr.strip(); even though this just a simple mapping... and not overwriting with like expr = "shit"...
436
+ "PLW3301", # min(x, min(y, z)) -> min(x, y, z), though this is unsafe if any x, y or z are float('nan')
437
+
438
+ # pyupgrade
439
+ "UP",
440
+
441
+ # refurb
442
+ "FURB101", # open().read() -> use pathlib
443
+ "FURB103", # open().write() -> use pathlib
444
+ "FURB105", # print("") -> print() ; though might be annoying if a warning pops up every fucking time i use print so we'll see if this sticks
445
+ "FURB110", # `x if x else y` -> `x or y`
446
+ "FURB113", # repeated append() -> extend()
447
+ "FURB116", # bin(1337)[:2] -> f'{1337:b}'
448
+ "FURB122", # try to use writelines() instead of write() in a loop if possible
449
+ "FURB129", # iteration over a `f` already invokes .readlines() by default, no need to call it explicitly
450
+ "FURB131", # prefer `x.clear()` over `del x[:]` for lists and dicts
451
+ "FURB132", # prefer `set.discard` over `set.remove`, when you know about the element's whereabouts anyway
452
+ "FURB136", # don't do manual min or max via if expr
453
+ "FURB142", # don't use set.add or set.discard in a loop, set.update or set.difference_update, or -=, |= operators tho the operators require set type on rhs as well
454
+ "FURB145", # prefer `x = lst.copy()` over `x = lst[:]`
455
+ "FURB148", # unnecessary enumerate
456
+ "FURB152", # detect hardcoded versions of math constants, prefer math.pi, math.e, etc.
457
+ "FURB156", # simillar to above but for string module constants
458
+ "FURB154", # `global` supports multiple identifierrs joined by a comma
459
+ "FURB157", # `Decimal` should be used without unnecessary proxies like float() or string when not needed
460
+ "FURB161", # manual int.bit_count()
461
+ "FURB162", # datetime.fromisoformat handling of timezone "Z"
462
+ "FURB163", # redundant math.log() base, use log2, log10, etc.
463
+ "FURB166", # use int(..., base=0) to detect the "0x", "0b", "0o" prefixes automatically instead of slicing, etc.
464
+ "FURB168", # use `x is None` instead of `isinstance(x, type(None))`
465
+ "FURB169", # use `x is None` instead of `type(obj) == type(None)`
466
+ "FURB177", # use pathlib.Path.cwd() instead of pathlib.Path().resolve()
467
+ "FURB180", # use abc.ABC instead of abc.ABCMeta
468
+ "FURB188", # startswith+slice -> removeprefix(), same for ends/suffix
469
+ "FURB189", # use User* types when wanting to inherit from a builtin type, since the builtins don't consistently use their own dunder methods
470
+ "FURB192", # manual min/max with sorted()[0]
471
+
472
+ # ruff
473
+ "RUF001", # confusable unicode characters
474
+ "RUF002", # confusable unicode characters in docstrings
475
+ "RUF003", # confusable unicode characters in comments
476
+ "RUF005", # concatenate collections using *splat
477
+ "RUF006", # asyncio event loop stores tasks in a weakref, you have to store it somewhere
478
+ "RUF007", # manual itertools.pairwise() with zip() and slicing
479
+ "RUF008", # mutable defaults in @dataclass'es and simillar
480
+ "RUF009", # same as above but the mutable default is coming from a function
481
+ "RUF010", # use f-strings' {!r} instead of manual repr() calls
482
+ "RUF012", # mutable classvar default
483
+ "RUF013", # x: int = None -> x: int | None = None
484
+ "RUF015", # prefer next(x) over list(x)[0]
485
+ "RUF016", # invalid index type, like indexing into a list with a str
486
+ "RUF017", # use better list flattening strategy
487
+ "RUF018", # dont use the wlarus operator in assert
488
+ "RUF019", # instead of `x in dct and dct[x]`, use `dct.get(x)`
489
+ "RUF020", # union with Never is same as the other side of the union by itself
490
+ "RUF021", # in expr `x or y and z` parenthesise the `y and z` part since `and` binds tigher than `or`.
491
+ "RUF022", # unsorted __all__
492
+ "RUF023", # unsorted __slots__
493
+ "RUF028", # ineffective `# fmt: off` and simillar comments
494
+ "RUF030", # assert with second argument being call to print should not be used
495
+ "RUF032", # decimal called from float literal
496
+ "RUF033", # use dataclasses.InitVar instead of init parameters' default value which dont work like that
497
+ "RUF034", # `x if y else x` -> `x`
498
+ "RUF036", # `None` should be at the end of a type union
499
+ "RUF037", # deque should not take empty iterable as first argument, as that's the default
500
+ "RUF038", # `Literal[True, False]` -> `bool`
501
+ "RUF039", # use r-str for regex calls
502
+ "RUF040", # assert second argument must a str
503
+ "RUF041", # unnecessary nested `Literal`
504
+ "RUF043", # pytest.raises(match=) shenanings
505
+ "RUF045", # implicit classvar in dataclass
506
+ "RUF046", # unnecessary cast to int
507
+ "RUF047", # empty `else` clause
508
+ "RUF048", # don't int-parse parts of version.split('.'), because it might not contain text like "post-1" which is not a valid integer
509
+ "RUF049", # don't use @dataclass on an enum.Enum class
510
+ "RUF050", # empty `if` clause
511
+ "RUF051", # `if key in dict: del dict[key]` -> `dict.pop(key, None)`
512
+ "RUF053", # type vars cause problems with Generic
513
+ "RUF054", # formfeed characters shouldn't be used
514
+ "RUF055", # regex could be replaced with simpler function
515
+ "RUF056", # falsey dict.get() fallback (the default of "None" is already falsey)
516
+ "RUF057", # unnecessary round (value is already an integer)
517
+ "RUF058", # starmap(f, zip(*...)) should be replaced with normal map
518
+ "RUF059", # unused unpacked variable
519
+ "RUF060", # `x in ()` or any empty collection
520
+ "RUF061", # legacy form pytest.raises
521
+ "RUF063", # correctly get annotations
522
+ "RUF064", # non-octal chmod permissions
523
+ "RUF065", # dont eagerly convert to str in logging
524
+ "RUF066", # @property without return
525
+ "RUF067", # __init__.py should only feature imports, docs and __all__
526
+ "RUF068", # duplicate entry in __all__
527
+ "RUF069", # float math should not use == or !=, but rather math.isclose
528
+ "RUF070", # unnecessary assign before yield
529
+ "RUF071", # os.path.commonprefix compares by char, os.path.commonpath compares by path part
530
+ "RUF072", # useless `finally` clause
531
+ "RUF074", # incorrect decorator order
532
+ "RUF075", # fallible @contextmanager
533
+ "RUF100", # unused noqa
534
+ "RUF101", # redirected noqa, use redirect target
535
+ "RUF102", # invalid noqa
536
+ "RUF103", # invalid suppression comment
537
+ "RUF104", # unmatched ruff: disable (add ruff: enable)
538
+ "RUF200", # pyproject.toml schema
539
+
540
+ # try
541
+ "TRY002", # create your own exception class
542
+ "TRY003", # exception messages should ideally be within the class __init__, not at class init site
543
+ "TRY004", # use TypeError for failed isinstance(x, T)
544
+ "TRY201", # use no-argument `raise` if possible
545
+ "TRY203", # useless try-except
546
+ "TRY300", # consider moving to else block
547
+ "TRY301", # raise within try shouldn't be used if possible
548
+ "TRY400", # logging.error->logging.exception when in except handler
549
+ "TRY401", # do not attach exception instance to logging.exception as it already has it
550
+ ]
551
+
552
+ ignore = [
553
+ "D105", # document magic methods, like __str__, this is just stupid. Why add a "Return a string representation of X"??? this is entirely obvious...
554
+ "D107", # document every public __init__. This is too much.
555
+ "TC003", # not moving to type checking block if im going to use it a second
556
+ ]
557
+
558
+ # ignore = [
559
+ # # "COM812", # missing-trailing-comma | So ruff fmt shuts up
560
+ # # "COM812", # single-line-implicit-string-concatenation | So ruff fmt shuts up
561
+
562
+ # "ANN401", # any-type | Dynamically typed expressions (typing.Any) are disallowed
563
+ # "DTZ001", # call-datetime-without-tzinfo | The use of datetime.datetime() without tzinfo argument is not allowed
564
+ # "DTZ005", # call-datetime-now-without-tzinfo | The use of datetime.datetime.now() without tzinfo argument is not allowed
565
+ # "RUF012", # mutable-class-default | Class utils no longer work so it's on the ignore list
566
+ # "ARG001", # unused-function-argument | Messed with voiding arguments so it's disabled
567
+ # "ARG002", # unused-method-argument | Messed with voiding arguments so it's disabled
568
+ # "ARG003", # unused-class-method-argument | Messed with voiding arguments so it's disabled
569
+ # "ARG005", # unused-lambd-argument | Messed with voiding arguments so it's disabled
570
+ # "ARG004", # unused-static-method-argument | Messed with voiding arguments so it's disabled
571
+ # # "PLR2004", # magic-value-comparison | Magic values bruh shut up
572
+ # "PLR0913", # too-many-arguments | Too many function arguments
573
+ # "RET505", # superfluous-else-return | This rule just messes up readability, bruh
574
+ # "TRY003", # raise-vanilla-args | Exception("asdf") -> msg = 'asdf'; Exception(msg) bruuuuuhhhhh
575
+ # "PTH109", # os-getcwd | os.getcwd() is fine
576
+ # "PYI041", # redundant-numeric-union | No
577
+ # "PIE796", # non-unique-enums | But then enums are dumb?
578
+ # "PIE790", # unnecessary-pass | Shut up
579
+ # "PIE800", # unnecessary-spread | no u
580
+ # "B008", # function-call-in-default-argument | This has its uses
581
+ # "B006", # Do not use mutable data structures for argument defaults # No, fuck off
582
+ # "RUF005", # collection-literal-concatenation | Shut up x2
583
+ # "A001", # builtin-variable-shadowing | id
584
+ # "A002", # builtin-argument-shadowing | type
585
+ # "A003", # builtin-attribute-shadowing | type
586
+ # "A005", # Module tcrutils/types.py is shadowing python's builtin...
587
+ # "TRY301", # raise-within-try | no i will not
588
+ # "RET503", # implicit-return | like wtf?
589
+ # "PIE804", # unnecessary-dict-kwargs | sometimes useful
590
+ # "RUF013", # implicit-optional | that's dumb
591
+ # "SIM105", # suppressible-exception | obnoxious
592
+ # "SIM108", # if-else-block-instead-of-if-exp | makes stuff less readable
593
+ # "UP032", # f-string | sometimes .format() is useful
594
+ # "UP031", # Use format specifiers instead of percent format | sometimes % is useful
595
+ # "SIM117", # multiple-with-statements | more readable the old way
596
+ # "SIM102", # collapsible-if | nah
597
+ # # "TCH001", # typing-only-first-party-import | move to type checking my ass
598
+ # # "TCH002",
599
+ # # "TCH003",
600
+
601
+ # "A004", # Overriding built-ins, from rich import print
602
+ # "RUF029", # async fn without awaits
603
+ # "I001", # I have a better import sorter
604
+ # "B026", # Star-arg unpacking after a keyword argument is strongly discouraged
605
+ # "FURB142", # Use of `set.add()` in a for loop
606
+ # ]
@@ -0,0 +1,9 @@
1
+ from .codeblock import codeblock, codeblocks, codeblocks_from_exception
2
+ from .uncodeblock import uncodeblock
3
+
4
+ __all__ = [
5
+ "codeblock",
6
+ "codeblocks",
7
+ "codeblocks_from_exception",
8
+ "uncodeblock",
9
+ ]
File without changes
@@ -0,0 +1 @@
1
+ __version__: str = "0.1.0"
@@ -0,0 +1,120 @@
1
+ from collections.abc import Callable
2
+
3
+ BACKTICK = "`"
4
+ BACKTICKS = 3 * BACKTICK
5
+ NEWLINE = "\n"
6
+
7
+
8
+ def _default_cut_at(text: str, max_length: int) -> str:
9
+ """Cut text at max_length, if text is longer than max_length.
10
+
11
+ Args:
12
+ text: The text to cut.
13
+ max_length: The maximum length of the output str.
14
+ Returns:
15
+ str: The text cut at max_length.
16
+ """
17
+ return text[:max_length]
18
+
19
+
20
+ def codeblock(
21
+ text: str,
22
+ *,
23
+ langcode: str = "",
24
+ max_length: int = 1984,
25
+ fn_cut_at: Callable[[str, int], str] = _default_cut_at,
26
+ smart_empty: bool = True,
27
+ convert_three_backticks_to_apostrophes: bool = False,
28
+ ) -> str:
29
+ r"""Pack text into a Discord codeblock. This will cut it to a max_length with a default for Discord, if you dont want this, set max_length to your desired length or -1 means text's length.
30
+
31
+ Note: May return str with len > max_length if max_length is ridicously low such that not even `'''\nlangcode'''` fits
32
+
33
+ Args:
34
+ text: The text to put in the codeblock body.
35
+ langcode: The language code to use in discord codeblock, for example "py".
36
+ max_length: The maximum length of the output str.
37
+ fn_cut_at: A Callable[[str, int], str] which's returned string is of or less than the passed in int in length.
38
+ smart_empty: If true, if `text` is '' (an empty string) it will be changed to ' ' (a space). This prevents the codeblock to think langcode is the body of the codeblock if no text is supplied.
39
+ convert_three_backticks_to_apostrophes: If true, any backticks in the text will be converted to apostrophes. This prevents the codeblock from being broken by backticks in the text.
40
+
41
+ Returns:
42
+ str: A str of the form ```langcode\ntext``` with a length of or less than max_length.
43
+ """
44
+ if smart_empty and not text:
45
+ text = " "
46
+
47
+ if convert_three_backticks_to_apostrophes:
48
+ text = text.replace(BACKTICKS, "'''")
49
+
50
+ length_of_parts = len(BACKTICKS) + len(langcode) + len(NEWLINE) + len(BACKTICKS)
51
+
52
+ return "".join((
53
+ BACKTICKS,
54
+ langcode,
55
+ NEWLINE,
56
+ fn_cut_at(
57
+ text,
58
+ (max_length - length_of_parts if max_length != -1 else len(text)),
59
+ ),
60
+ BACKTICKS,
61
+ ))
62
+
63
+
64
+ def codeblocks(
65
+ text0: str,
66
+ *texts: str,
67
+ langcodes: tuple[str, ...] | None = None,
68
+ max_length: int = 1984,
69
+ fn_cut_at: Callable[[str, int], str] = _default_cut_at,
70
+ smart_empty: bool = True,
71
+ convert_three_backticks_to_apostrophes: bool = False,
72
+ ) -> str:
73
+ r"""Mutliple-`codeblock()`. For more info read codeblock() docstring.
74
+
75
+ Args:
76
+ text0: The first text to put in the codeblock body.
77
+ *texts: The rest of the texts to put in the codeblock body.
78
+ langcodes: The language codes to use in discord codeblock, for example "py". If None, all langcodes will be empty strings. If provided, must be of same length as the total amount of texts passed in including text0.
79
+ max_length: The maximum length of the output str.
80
+ fn_cut_at: A Callable[[str, int], str] which's returned string is of or less than the passed in int in length.
81
+ smart_empty: If true, if `text` is '' (an empty string) it will be changed to ' ' (a space). This prevents the codeblock to think langcode is the body of the codeblock if no text is supplied.
82
+
83
+ Returns:
84
+ str: A str of the form ```langcode\ntext``` with a length of or less than max_length.
85
+ """
86
+ texts = text0, *texts
87
+
88
+ if langcodes is None:
89
+ langcodes: tuple[str, ...] = ("",) * len(texts)
90
+
91
+ out = ""
92
+
93
+ for text, langcode in zip(texts, langcodes, strict=True):
94
+ out += codeblock(
95
+ text,
96
+ langcode=langcode,
97
+ max_length=max_length - len(out),
98
+ fn_cut_at=fn_cut_at,
99
+ smart_empty=smart_empty,
100
+ convert_three_backticks_to_apostrophes=convert_three_backticks_to_apostrophes,
101
+ )
102
+
103
+ return out
104
+
105
+
106
+ def codeblocks_from_exception(
107
+ e: BaseException,
108
+ *,
109
+ max_length: int = 1800,
110
+ ) -> str:
111
+ return codeblocks(
112
+ extract_error(e),
113
+ extract_traceback(e),
114
+ langcodes=(
115
+ "txt",
116
+ "py",
117
+ ),
118
+ max_length=max_length,
119
+ smart_empty=True,
120
+ )
@@ -0,0 +1,19 @@
1
+ from .codeblock import BACKTICKS, NEWLINE
2
+
3
+
4
+ def uncodeblock(text: str) -> str:
5
+ """*Try to* remove any markdown/discord codeblocks along with their langcodes if any are found.
6
+
7
+ Args:
8
+ text: The text to remove markdown/discord codeblocks from.
9
+
10
+ Returns:
11
+ str: The text with any markdown/discord codeblocks removed.
12
+ """
13
+ if text[-3:] == BACKTICKS and text[:3] == BACKTICKS:
14
+ code_start = 3
15
+ code_end = -3
16
+ if NEWLINE in text[3:]: # Check if there is a language code specified
17
+ code_start = text.index(NEWLINE) + 1
18
+ return text[code_start:code_end].strip()
19
+ return text
File without changes
@@ -0,0 +1,25 @@
1
+ from nya_codeblock import codeblock
2
+
3
+
4
+ def test_codeblock_simple():
5
+ result = codeblock("Hello, World!")
6
+ assert result == "```\nHello, World!```"
7
+
8
+
9
+ def test_codeblock_with_langcode():
10
+ result = codeblock("print('Hello, World!')", langcode="python")
11
+ assert result == "```python\nprint('Hello, World!')```"
12
+
13
+
14
+ def test_codeblock_max_length():
15
+ result = codeblock("This is a long text that should be cut.", max_length=24)
16
+ assert result == "```\nThis is a long te```"
17
+ assert len(result) <= 24
18
+
19
+
20
+ def test_codeblock_smart_empty():
21
+ with_smart_empty = codeblock("", smart_empty=True)
22
+ assert with_smart_empty == "```\n ```"
23
+
24
+ without_smart_empty = codeblock("", smart_empty=False)
25
+ assert without_smart_empty == "```\n```"
@@ -0,0 +1,78 @@
1
+ version = 1
2
+ revision = 3
3
+ requires-python = ">=3.11"
4
+
5
+ [[package]]
6
+ name = "colorama"
7
+ version = "0.4.6"
8
+ source = { registry = "https://pypi.org/simple" }
9
+ sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
10
+ wheels = [
11
+ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
12
+ ]
13
+
14
+ [[package]]
15
+ name = "iniconfig"
16
+ version = "2.3.0"
17
+ source = { registry = "https://pypi.org/simple" }
18
+ sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
19
+ wheels = [
20
+ { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
21
+ ]
22
+
23
+ [[package]]
24
+ name = "nya-codeblock"
25
+ source = { editable = "." }
26
+
27
+ [package.dev-dependencies]
28
+ dev = [
29
+ { name = "pytest" },
30
+ ]
31
+
32
+ [package.metadata]
33
+
34
+ [package.metadata.requires-dev]
35
+ dev = [{ name = "pytest", specifier = ">=9.1.1" }]
36
+
37
+ [[package]]
38
+ name = "packaging"
39
+ version = "26.2"
40
+ source = { registry = "https://pypi.org/simple" }
41
+ sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" }
42
+ wheels = [
43
+ { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" },
44
+ ]
45
+
46
+ [[package]]
47
+ name = "pluggy"
48
+ version = "1.6.0"
49
+ source = { registry = "https://pypi.org/simple" }
50
+ sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
51
+ wheels = [
52
+ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
53
+ ]
54
+
55
+ [[package]]
56
+ name = "pygments"
57
+ version = "2.20.0"
58
+ source = { registry = "https://pypi.org/simple" }
59
+ sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" }
60
+ wheels = [
61
+ { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" },
62
+ ]
63
+
64
+ [[package]]
65
+ name = "pytest"
66
+ version = "9.1.1"
67
+ source = { registry = "https://pypi.org/simple" }
68
+ dependencies = [
69
+ { name = "colorama", marker = "sys_platform == 'win32'" },
70
+ { name = "iniconfig" },
71
+ { name = "packaging" },
72
+ { name = "pluggy" },
73
+ { name = "pygments" },
74
+ ]
75
+ sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" }
76
+ wheels = [
77
+ { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" },
78
+ ]