dycw-utilities 0.125.3__py3-none-any.whl → 0.125.4__py3-none-any.whl

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dycw-utilities
3
- Version: 0.125.3
3
+ Version: 0.125.4
4
4
  Author-email: Derek Wan <d.wan@icloud.com>
5
5
  License-File: LICENSE
6
6
  Requires-Python: >=3.12
@@ -10,7 +10,7 @@ Requires-Dist: hypothesis<6.132,>=6.131.27; extra == 'test'
10
10
  Requires-Dist: pytest-asyncio<0.27,>=0.26.0; extra == 'test'
11
11
  Requires-Dist: pytest-cov<6.2,>=6.1.1; extra == 'test'
12
12
  Requires-Dist: pytest-instafail<0.6,>=0.5.0; extra == 'test'
13
- Requires-Dist: pytest-lazy-fixtures<1.2,>=1.1.2; extra == 'test'
13
+ Requires-Dist: pytest-lazy-fixtures<1.2,>=1.1.3; extra == 'test'
14
14
  Requires-Dist: pytest-only<2.2,>=2.1.2; extra == 'test'
15
15
  Requires-Dist: pytest-randomly<3.17,>=3.16.0; extra == 'test'
16
16
  Requires-Dist: pytest-regressions<2.8,>=2.7.0; extra == 'test'
@@ -174,7 +174,7 @@ Requires-Dist: scipy<1.16,>=1.15.3; extra == 'zzz-test-scipy'
174
174
  Provides-Extra: zzz-test-sentinel
175
175
  Provides-Extra: zzz-test-shelve
176
176
  Provides-Extra: zzz-test-slack-sdk
177
- Requires-Dist: aiohttp<3.12,>=3.11.16; extra == 'zzz-test-slack-sdk'
177
+ Requires-Dist: aiohttp<3.13,>=3.12.0; extra == 'zzz-test-slack-sdk'
178
178
  Requires-Dist: slack-sdk<3.36,>=3.35.0; extra == 'zzz-test-slack-sdk'
179
179
  Provides-Extra: zzz-test-socket
180
180
  Provides-Extra: zzz-test-sqlalchemy
@@ -1,4 +1,4 @@
1
- utilities/__init__.py,sha256=S9hi5nUNGrmUoeHSNNw9CdOdoY68OUGHaOm0V7chzZo,60
1
+ utilities/__init__.py,sha256=O9WCgPzIqylIlRxvuTI9h_FJzryWHiUMdYDZGV6Rhj8,60
2
2
  utilities/altair.py,sha256=Gpja-flOo-Db0PIPJLJsgzAlXWoKUjPU1qY-DQ829ek,9156
3
3
  utilities/asyncio.py,sha256=gr2eUx0E6LiCup6VKgUGwh8lAUriGdX2TlK-PZdlvfo,28284
4
4
  utilities/atomicwrites.py,sha256=geFjn9Pwn-tTrtoGjDDxWli9NqbYfy3gGL6ZBctiqSo,5393
@@ -28,6 +28,7 @@ utilities/importlib.py,sha256=mV1xT_O_zt_GnZZ36tl3xOmMaN_3jErDWY54fX39F6Y,429
28
28
  utilities/ipython.py,sha256=V2oMYHvEKvlNBzxDXdLvKi48oUq2SclRg5xasjaXStw,763
29
29
  utilities/iterables.py,sha256=prKXBdF5QfLTGC-q4567DwO8xzUng_Z-2a4wBkMqyDo,45360
30
30
  utilities/jupyter.py,sha256=ft5JA7fBxXKzP-L9W8f2-wbF0QeYc_2uLQNFDVk4Z-M,2917
31
+ utilities/libcst.py,sha256=jGLm2vTwUFcCIghN66mDSUXesgZZ6Pw2BbixkUf-PHA,5062
31
32
  utilities/lightweight_charts.py,sha256=vyVOzarYhBIOZj2xDhqdbP85qbSKUjdc6Au91rc1W4M,2814
32
33
  utilities/logging.py,sha256=gwo3pusPjnWO1ollrtn1VKYyRAQJTue4SkCbMeNvec4,25715
33
34
  utilities/loguru.py,sha256=MEMQVWrdECxk1e3FxGzmOf21vWT9j8CAir98SEXFKPA,3809
@@ -87,7 +88,7 @@ utilities/warnings.py,sha256=un1LvHv70PU-LLv8RxPVmugTzDJkkGXRMZTE2-fTQHw,1771
87
88
  utilities/whenever.py,sha256=jS31ZAY5OMxFxLja_Yo5Fidi87Pd-GoVZ7Vi_teqVDA,16743
88
89
  utilities/zipfile.py,sha256=24lQc9ATcJxHXBPc_tBDiJk48pWyRrlxO2fIsFxU0A8,699
89
90
  utilities/zoneinfo.py,sha256=-5j7IQ9nb7gR43rdgA7ms05im-XuqhAk9EJnQBXxCoQ,1874
90
- dycw_utilities-0.125.3.dist-info/METADATA,sha256=vMdoTZVKyi-BNSOGgeJYjY6uY-icfbVmvUlAo_SwJmM,12852
91
- dycw_utilities-0.125.3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
92
- dycw_utilities-0.125.3.dist-info/licenses/LICENSE,sha256=gppZp16M6nSVpBbUBrNL6JuYfvKwZiKgV7XoKKsHzqo,1066
93
- dycw_utilities-0.125.3.dist-info/RECORD,,
91
+ dycw_utilities-0.125.4.dist-info/METADATA,sha256=dwOemW5k0Dz8lVM3bRKczDGMMkRNTW41Np8Bi_13dEU,12851
92
+ dycw_utilities-0.125.4.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
93
+ dycw_utilities-0.125.4.dist-info/licenses/LICENSE,sha256=gppZp16M6nSVpBbUBrNL6JuYfvKwZiKgV7XoKKsHzqo,1066
94
+ dycw_utilities-0.125.4.dist-info/RECORD,,
utilities/__init__.py CHANGED
@@ -1,3 +1,3 @@
1
1
  from __future__ import annotations
2
2
 
3
- __version__ = "0.125.3"
3
+ __version__ = "0.125.4"
utilities/libcst.py ADDED
@@ -0,0 +1,184 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Sequence
4
+ from dataclasses import dataclass
5
+ from subprocess import check_output
6
+ from typing import assert_never, override
7
+
8
+ from libcst import (
9
+ AsName,
10
+ Attribute,
11
+ BaseExpression,
12
+ FormattedString,
13
+ FormattedStringExpression,
14
+ FormattedStringText,
15
+ Import,
16
+ ImportAlias,
17
+ ImportFrom,
18
+ ImportStar,
19
+ Module,
20
+ Name,
21
+ )
22
+
23
+
24
+ def generate_from_import(
25
+ module: str, name: str, /, *, asname: str | None = None
26
+ ) -> ImportFrom:
27
+ """Generate an `ImportFrom` object."""
28
+ alias = ImportAlias(
29
+ name=Name(name), asname=AsName(Name(asname)) if asname else None
30
+ )
31
+ return ImportFrom(module=split_dotted_str(module), names=[alias])
32
+
33
+
34
+ def generate_f_string(var: str, suffix: str, /) -> FormattedString:
35
+ """Generate an f-string."""
36
+ return FormattedString([
37
+ FormattedStringExpression(expression=Name(var)),
38
+ FormattedStringText(suffix),
39
+ ])
40
+
41
+
42
+ def generate_import(module: str, /, *, asname: str | None = None) -> Import:
43
+ """Generate an `Import` object."""
44
+ alias = ImportAlias(
45
+ name=split_dotted_str(module), asname=AsName(Name(asname)) if asname else None
46
+ )
47
+ return Import(names=[alias])
48
+
49
+
50
+ ##
51
+
52
+
53
+ @dataclass(kw_only=True, slots=True)
54
+ class _ParseImportOutput:
55
+ module: str
56
+ name: str | None = None
57
+
58
+
59
+ def parse_import(import_: Import | ImportFrom, /) -> Sequence[_ParseImportOutput]:
60
+ """Parse an import."""
61
+ match import_:
62
+ case Import():
63
+ return [_parse_import_one(n) for n in import_.names]
64
+ case ImportFrom():
65
+ if (attr_or_name := import_.module) is None:
66
+ raise _ParseImportEmptyModuleError(import_=import_)
67
+ module = join_dotted_str(attr_or_name)
68
+ match import_.names:
69
+ case Sequence() as names:
70
+ return [_parse_import_from_one(module, n) for n in names]
71
+ case ImportStar():
72
+ return [_ParseImportOutput(module=module, name="*")]
73
+ case _ as never:
74
+ assert_never(never)
75
+ case _ as never:
76
+ assert_never(never)
77
+
78
+
79
+ def _parse_import_one(alias: ImportAlias, /) -> _ParseImportOutput:
80
+ return _ParseImportOutput(module=join_dotted_str(alias.name))
81
+
82
+
83
+ def _parse_import_from_one(module: str, alias: ImportAlias, /) -> _ParseImportOutput:
84
+ match alias.name:
85
+ case Name(name):
86
+ return _ParseImportOutput(module=module, name=name)
87
+ case Attribute() as attr:
88
+ raise _ParseImportAliasError(module=module, attr=attr)
89
+ case _ as never:
90
+ assert_never(never)
91
+
92
+
93
+ @dataclass(kw_only=True, slots=True)
94
+ class ParseImportError(Exception): ...
95
+
96
+
97
+ @dataclass(kw_only=True, slots=True)
98
+ class _ParseImportEmptyModuleError(ParseImportError):
99
+ import_: ImportFrom
100
+
101
+ @override
102
+ def __str__(self) -> str:
103
+ return f"Module must not be None; got {self.import_}"
104
+
105
+
106
+ @dataclass(kw_only=True, slots=True)
107
+ class _ParseImportAliasError(ParseImportError):
108
+ module: str
109
+ attr: Attribute
110
+
111
+ @override
112
+ def __str__(self) -> str:
113
+ attr = self.attr
114
+ return f"Invalid alias name; got module {self.module!r} and attribute '{attr.value.value}.{attr.attr.value}'"
115
+
116
+
117
+ ##
118
+
119
+
120
+ def split_dotted_str(dotted: str, /) -> Name | Attribute:
121
+ """Split a dotted string into a name/attribute."""
122
+ parts = dotted.split(".")
123
+ node = Name(parts[0])
124
+ for part in parts[1:]:
125
+ node = Attribute(value=node, attr=Name(part))
126
+ return node
127
+
128
+
129
+ def join_dotted_str(name_or_attr: Name | Attribute, /) -> str:
130
+ """Join a dotted from from a name/attribute."""
131
+ parts: Sequence[str] = []
132
+ curr: BaseExpression | Name | Attribute = name_or_attr
133
+ while True:
134
+ match curr:
135
+ case Name(value=value):
136
+ parts.append(value)
137
+ break
138
+ case Attribute(value=value, attr=Name(value=attr_value)):
139
+ parts.append(attr_value)
140
+ curr = value
141
+ case BaseExpression() as expr:
142
+ raise JoinDottedStrError(name_or_attr=name_or_attr, expr=expr)
143
+ case _ as never:
144
+ assert_never(never)
145
+ return ".".join(reversed(parts))
146
+
147
+
148
+ @dataclass(kw_only=True, slots=True)
149
+ class JoinDottedStrError(Exception):
150
+ name_or_attr: Name | Attribute
151
+ expr: BaseExpression
152
+
153
+ @override
154
+ def __str__(self) -> str:
155
+ return f"Only names & attributes allowed; got {self.expr}"
156
+
157
+
158
+ ##
159
+
160
+
161
+ def render_module(source: str | Module, /) -> str:
162
+ """Render a module."""
163
+ match source: # skipif-ci
164
+ case str() as text:
165
+ return check_output(["ruff", "format", "-"], input=text, text=True)
166
+ case Module() as module:
167
+ return render_module(module.code)
168
+ case _ as never:
169
+ assert_never(never)
170
+
171
+
172
+ ##
173
+
174
+
175
+ __all__ = [
176
+ "ParseImportError",
177
+ "generate_f_string",
178
+ "generate_from_import",
179
+ "generate_import",
180
+ "join_dotted_str",
181
+ "parse_import",
182
+ "render_module",
183
+ "split_dotted_str",
184
+ ]