textpy 0.0.0__py2.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.
- textpy/__init__.py +55 -0
- textpy/__version__.py +3 -0
- textpy/abc.py +452 -0
- textpy/core.py +48 -0
- textpy/element.py +227 -0
- textpy/format.py +21 -0
- textpy/utils/__init__.py +16 -0
- textpy/utils/re_extended.py +237 -0
- textpy-0.0.0.dist-info/LICENSE +24 -0
- textpy-0.0.0.dist-info/METADATA +94 -0
- textpy-0.0.0.dist-info/RECORD +13 -0
- textpy-0.0.0.dist-info/WHEEL +6 -0
- textpy-0.0.0.dist-info/top_level.txt +1 -0
textpy/__init__.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import warnings
|
|
2
|
+
|
|
3
|
+
__all__ = []
|
|
4
|
+
|
|
5
|
+
# import `abc` if exists
|
|
6
|
+
try:
|
|
7
|
+
from . import abc
|
|
8
|
+
|
|
9
|
+
__all__.extend(abc.__all__)
|
|
10
|
+
from .abc import *
|
|
11
|
+
|
|
12
|
+
except ImportError as e:
|
|
13
|
+
if isinstance(e, ModuleNotFoundError):
|
|
14
|
+
raise e
|
|
15
|
+
else:
|
|
16
|
+
warnings.warn(e.msg, Warning)
|
|
17
|
+
|
|
18
|
+
# import `core` if exists
|
|
19
|
+
try:
|
|
20
|
+
from . import core
|
|
21
|
+
|
|
22
|
+
__all__.extend(core.__all__)
|
|
23
|
+
from .core import *
|
|
24
|
+
|
|
25
|
+
except ImportError as e:
|
|
26
|
+
if isinstance(e, ModuleNotFoundError):
|
|
27
|
+
raise e
|
|
28
|
+
else:
|
|
29
|
+
warnings.warn(e.msg, Warning)
|
|
30
|
+
|
|
31
|
+
# import `element` if exists
|
|
32
|
+
try:
|
|
33
|
+
from . import element
|
|
34
|
+
|
|
35
|
+
__all__.extend(element.__all__)
|
|
36
|
+
from .element import *
|
|
37
|
+
|
|
38
|
+
except ImportError as e:
|
|
39
|
+
if isinstance(e, ModuleNotFoundError):
|
|
40
|
+
raise e
|
|
41
|
+
else:
|
|
42
|
+
warnings.warn(e.msg, Warning)
|
|
43
|
+
|
|
44
|
+
# import `format` if exists
|
|
45
|
+
try:
|
|
46
|
+
from . import format
|
|
47
|
+
|
|
48
|
+
__all__.extend(format.__all__)
|
|
49
|
+
from .format import *
|
|
50
|
+
|
|
51
|
+
except ImportError as e:
|
|
52
|
+
if isinstance(e, ModuleNotFoundError):
|
|
53
|
+
raise e
|
|
54
|
+
else:
|
|
55
|
+
warnings.warn(e.msg, Warning)
|
textpy/__version__.py
ADDED
textpy/abc.py
ADDED
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from abc import ABC, abstractclassmethod
|
|
3
|
+
from functools import cached_property
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import *
|
|
6
|
+
|
|
7
|
+
import attrs
|
|
8
|
+
import pandas as pd
|
|
9
|
+
from pandas.io.formats.style import Styler
|
|
10
|
+
|
|
11
|
+
from .utils.re_extended import pattern_inreg, real_findall
|
|
12
|
+
|
|
13
|
+
__all__ = ["PyText", "Docstring"]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@attrs.define(auto_attribs=False)
|
|
17
|
+
class PyText(ABC):
|
|
18
|
+
text: str = ""
|
|
19
|
+
name: str = ""
|
|
20
|
+
path: Path = Path("NULL.py")
|
|
21
|
+
parent: Union["PyText", None] = None
|
|
22
|
+
start_line: int = 0
|
|
23
|
+
spaces: int = 0
|
|
24
|
+
|
|
25
|
+
@abstractclassmethod
|
|
26
|
+
def __init__(self):
|
|
27
|
+
"""Abstract class for python code analysis."""
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
def __repr__(self):
|
|
31
|
+
return f"{self.__class__.__name__}('{self.absname}')"
|
|
32
|
+
|
|
33
|
+
@cached_property
|
|
34
|
+
def doc(self) -> "Docstring":
|
|
35
|
+
"""
|
|
36
|
+
Docstring of a function / class / method.
|
|
37
|
+
|
|
38
|
+
Returns
|
|
39
|
+
-------
|
|
40
|
+
Docstring
|
|
41
|
+
An instance of `Docstring`.
|
|
42
|
+
|
|
43
|
+
"""
|
|
44
|
+
return Docstring("")
|
|
45
|
+
|
|
46
|
+
@cached_property
|
|
47
|
+
def header(self) -> "PyText":
|
|
48
|
+
"""
|
|
49
|
+
Header of a class or a file.
|
|
50
|
+
|
|
51
|
+
Returns
|
|
52
|
+
-------
|
|
53
|
+
TextPy
|
|
54
|
+
An instance of `TextPy`.
|
|
55
|
+
|
|
56
|
+
"""
|
|
57
|
+
return self.__class__("", parent=self)
|
|
58
|
+
|
|
59
|
+
@cached_property
|
|
60
|
+
def children_dict(self) -> Dict[str, "PyText"]:
|
|
61
|
+
"""
|
|
62
|
+
Dictionary of children nodes.
|
|
63
|
+
|
|
64
|
+
Returns
|
|
65
|
+
-------
|
|
66
|
+
Dict[str, TextPy]
|
|
67
|
+
Dictionary of children nodes.
|
|
68
|
+
|
|
69
|
+
"""
|
|
70
|
+
return {}
|
|
71
|
+
|
|
72
|
+
@cached_property
|
|
73
|
+
def children(self) -> List["PyText"]:
|
|
74
|
+
"""
|
|
75
|
+
List of children nodes.
|
|
76
|
+
|
|
77
|
+
Returns
|
|
78
|
+
-------
|
|
79
|
+
Dict[str, TextPy]
|
|
80
|
+
List of children nodes.
|
|
81
|
+
|
|
82
|
+
"""
|
|
83
|
+
return list(self.children_dict.values())
|
|
84
|
+
|
|
85
|
+
@cached_property
|
|
86
|
+
def absname(self) -> str:
|
|
87
|
+
"""
|
|
88
|
+
Returns a full-name including all the parent's name, connected with
|
|
89
|
+
`"."`'s.
|
|
90
|
+
|
|
91
|
+
Returns
|
|
92
|
+
-------
|
|
93
|
+
str
|
|
94
|
+
Absolute name.
|
|
95
|
+
|
|
96
|
+
"""
|
|
97
|
+
if self.parent is None:
|
|
98
|
+
return self.name
|
|
99
|
+
else:
|
|
100
|
+
return self.parent.absname + ("." + self.name).replace(".NULL", "")
|
|
101
|
+
|
|
102
|
+
@cached_property
|
|
103
|
+
def relname(self) -> str:
|
|
104
|
+
"""
|
|
105
|
+
Differences to `absname` that it doesn't include the top parent's name.
|
|
106
|
+
|
|
107
|
+
Returns
|
|
108
|
+
-------
|
|
109
|
+
str
|
|
110
|
+
Relative name.
|
|
111
|
+
|
|
112
|
+
"""
|
|
113
|
+
return self.absname.split(".", maxsplit=1)[-1]
|
|
114
|
+
|
|
115
|
+
@cached_property
|
|
116
|
+
def abspath(self) -> Path:
|
|
117
|
+
"""
|
|
118
|
+
The absolute path of `self`.
|
|
119
|
+
|
|
120
|
+
Returns
|
|
121
|
+
-------
|
|
122
|
+
Path
|
|
123
|
+
Absolute path.
|
|
124
|
+
|
|
125
|
+
"""
|
|
126
|
+
return self.path.absolute()
|
|
127
|
+
|
|
128
|
+
@cached_property
|
|
129
|
+
def relpath(self) -> Path:
|
|
130
|
+
"""
|
|
131
|
+
Find the relative path to `home`.
|
|
132
|
+
|
|
133
|
+
Returns
|
|
134
|
+
-------
|
|
135
|
+
Path
|
|
136
|
+
Relative path.
|
|
137
|
+
|
|
138
|
+
"""
|
|
139
|
+
if self.path.stem == "NULL":
|
|
140
|
+
return self.path if self.parent is None else self.parent.relpath
|
|
141
|
+
else:
|
|
142
|
+
return self.path.absolute().relative_to(self.path.home())
|
|
143
|
+
|
|
144
|
+
def findall(
|
|
145
|
+
self, pattern: str, regex: bool = True, styler: bool = True
|
|
146
|
+
) -> Union[Styler, "FindTextResult"]:
|
|
147
|
+
"""
|
|
148
|
+
Search for `pattern`.
|
|
149
|
+
|
|
150
|
+
Parameters
|
|
151
|
+
----------
|
|
152
|
+
pattern : str
|
|
153
|
+
Pattern string.
|
|
154
|
+
regex : bool, optional
|
|
155
|
+
Whether to use regular expression, by default True.
|
|
156
|
+
styler : bool, optional
|
|
157
|
+
Whether to return a `Styler` object in convenience of displaying
|
|
158
|
+
in a Jupyter notebook, by default True.
|
|
159
|
+
|
|
160
|
+
Returns
|
|
161
|
+
-------
|
|
162
|
+
Union[Styler, FindTextResult]
|
|
163
|
+
Searching result.
|
|
164
|
+
|
|
165
|
+
Raises
|
|
166
|
+
------
|
|
167
|
+
ValueError
|
|
168
|
+
Raised when `pattern` ends with a '\\\\'.
|
|
169
|
+
|
|
170
|
+
"""
|
|
171
|
+
if len(pattern) > 0 and pattern[-1] == "\\":
|
|
172
|
+
raise ValueError(f"pattern should not end with a '\\': {pattern}")
|
|
173
|
+
if not regex:
|
|
174
|
+
pattern = pattern_inreg(pattern)
|
|
175
|
+
res = FindTextResult(pattern)
|
|
176
|
+
if self.children == []:
|
|
177
|
+
to_match = self.text
|
|
178
|
+
for _line, _, _group in real_findall(
|
|
179
|
+
".*" + pattern + ".*", to_match, linemode=True
|
|
180
|
+
):
|
|
181
|
+
if _group != "":
|
|
182
|
+
res.append((self, self.start_line + _line - 1, _group))
|
|
183
|
+
else:
|
|
184
|
+
res = res.join(self.header.findall(pattern, styler=False))
|
|
185
|
+
for c in self.children:
|
|
186
|
+
res = res.join(c.findall(pattern, styler=False))
|
|
187
|
+
if styler:
|
|
188
|
+
return res.to_styler()
|
|
189
|
+
else:
|
|
190
|
+
return res
|
|
191
|
+
|
|
192
|
+
def jumpto(self, target: str) -> "PyText":
|
|
193
|
+
"""
|
|
194
|
+
Jump to another `TextPy` instance.
|
|
195
|
+
|
|
196
|
+
Parameters
|
|
197
|
+
----------
|
|
198
|
+
target : str
|
|
199
|
+
Relative name of the target instance.
|
|
200
|
+
|
|
201
|
+
Returns
|
|
202
|
+
-------
|
|
203
|
+
TextPy
|
|
204
|
+
An instance of `TextPy`.
|
|
205
|
+
|
|
206
|
+
Raises
|
|
207
|
+
------
|
|
208
|
+
ValueError
|
|
209
|
+
Raised when `target` doesn't exist.
|
|
210
|
+
|
|
211
|
+
"""
|
|
212
|
+
if target == "":
|
|
213
|
+
return self
|
|
214
|
+
splits = re.split("\.", target, maxsplit=1)
|
|
215
|
+
if len(splits) == 1:
|
|
216
|
+
splits.append("")
|
|
217
|
+
if self.name == splits[0]:
|
|
218
|
+
return self.jumpto(splits[1])
|
|
219
|
+
elif splits[0] == "":
|
|
220
|
+
if self.parent is not None:
|
|
221
|
+
return self.parent.jumpto(splits[1])
|
|
222
|
+
raise ValueError(f"`{self.absname}` hasn't got a parent")
|
|
223
|
+
else:
|
|
224
|
+
if splits[0] in self.children_dict:
|
|
225
|
+
return self.children_dict[splits[0]].jumpto(splits[1])
|
|
226
|
+
raise ValueError(f"`{splits[0]}` is not a child of `{self.absname}`")
|
|
227
|
+
|
|
228
|
+
def as_header(self):
|
|
229
|
+
"""
|
|
230
|
+
Declare `self` as a class header (rather than the class itself).
|
|
231
|
+
|
|
232
|
+
Returns
|
|
233
|
+
-------
|
|
234
|
+
self
|
|
235
|
+
An instance of self.
|
|
236
|
+
|
|
237
|
+
"""
|
|
238
|
+
self.name = "NULL"
|
|
239
|
+
return self
|
|
240
|
+
|
|
241
|
+
def track(self) -> List["PyText"]:
|
|
242
|
+
"""
|
|
243
|
+
Returns a list of all the parents and `self`.
|
|
244
|
+
|
|
245
|
+
Returns
|
|
246
|
+
-------
|
|
247
|
+
List[TextPy]
|
|
248
|
+
A list of `TextPy` instances.
|
|
249
|
+
|
|
250
|
+
"""
|
|
251
|
+
track: List["PyText"] = []
|
|
252
|
+
obj: Union["PyText", None] = self
|
|
253
|
+
while obj is not None:
|
|
254
|
+
track.append(obj)
|
|
255
|
+
obj = obj.parent
|
|
256
|
+
track.reverse()
|
|
257
|
+
return track
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
class Docstring(ABC):
|
|
261
|
+
def __init__(self, text: str, parent: Union[PyText, None] = None):
|
|
262
|
+
"""
|
|
263
|
+
Stores the docstring of a function / class / method, then divides
|
|
264
|
+
it into different sections accaording to its titles.
|
|
265
|
+
|
|
266
|
+
Parameters
|
|
267
|
+
----------
|
|
268
|
+
text : str
|
|
269
|
+
Docstring text.
|
|
270
|
+
parent : Union[TextPy, None], optional
|
|
271
|
+
Parent node (if exists), by default None.
|
|
272
|
+
|
|
273
|
+
"""
|
|
274
|
+
self.text = text.strip()
|
|
275
|
+
self.parent = parent
|
|
276
|
+
|
|
277
|
+
@cached_property
|
|
278
|
+
@abstractclassmethod
|
|
279
|
+
def sections(self) -> Dict[str, str]:
|
|
280
|
+
"""
|
|
281
|
+
Returns the details of the docstring, each title corresponds to a
|
|
282
|
+
paragraph of description.
|
|
283
|
+
|
|
284
|
+
Returns
|
|
285
|
+
-------
|
|
286
|
+
Dict[str, str]
|
|
287
|
+
Dict of titles and descriptions.
|
|
288
|
+
|
|
289
|
+
"""
|
|
290
|
+
return {}
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
class FindTextResult:
|
|
294
|
+
def __init__(self, pattern: str):
|
|
295
|
+
"""
|
|
296
|
+
Result of text finding, only as a return of `TextPy.find_text`.
|
|
297
|
+
|
|
298
|
+
Parameters
|
|
299
|
+
----------
|
|
300
|
+
pattern : str
|
|
301
|
+
Pattern string.
|
|
302
|
+
|
|
303
|
+
"""
|
|
304
|
+
self.pattern = pattern
|
|
305
|
+
self.res: List[Tuple[PyText, int, str]] = []
|
|
306
|
+
|
|
307
|
+
def __repr__(self) -> str:
|
|
308
|
+
string: str = ""
|
|
309
|
+
for _tp, _n, _group in self.res:
|
|
310
|
+
string += f"\n{_tp.relpath}:{_n}: "
|
|
311
|
+
_sub = re.sub(
|
|
312
|
+
self.pattern,
|
|
313
|
+
lambda x: "\033[100m" + x.group() + "\033[0m",
|
|
314
|
+
" " * _tp.spaces + _group,
|
|
315
|
+
)
|
|
316
|
+
string += re.sub("\\\\x1b\[", "\033[", _sub.__repr__())
|
|
317
|
+
return string.lstrip()
|
|
318
|
+
|
|
319
|
+
def append(self, finding: Tuple[PyText, int, str]):
|
|
320
|
+
"""
|
|
321
|
+
Append a new finding.
|
|
322
|
+
|
|
323
|
+
Parameters
|
|
324
|
+
----------
|
|
325
|
+
finding : Tuple[TextPy, int, str]
|
|
326
|
+
Contains a `TextPy` instance, the line number where pattern
|
|
327
|
+
is found, and a matched string.
|
|
328
|
+
|
|
329
|
+
"""
|
|
330
|
+
self.res.append(finding)
|
|
331
|
+
|
|
332
|
+
def extend(self, findings: List[Tuple[PyText, int, str]]):
|
|
333
|
+
"""
|
|
334
|
+
Extend a few new findings.
|
|
335
|
+
|
|
336
|
+
Parameters
|
|
337
|
+
----------
|
|
338
|
+
findings : List[Tuple[TextPy, int, str]]
|
|
339
|
+
A finding contains a `TextPy` instance, the line number where
|
|
340
|
+
pattern is found, and a matched string.
|
|
341
|
+
|
|
342
|
+
"""
|
|
343
|
+
self.res.extend(findings)
|
|
344
|
+
|
|
345
|
+
def join(self, other: "FindTextResult") -> "FindTextResult":
|
|
346
|
+
"""
|
|
347
|
+
Joins two `FindTextResult` instance, only works when they share the
|
|
348
|
+
same `pattern`.
|
|
349
|
+
|
|
350
|
+
Parameters
|
|
351
|
+
----------
|
|
352
|
+
other : FindTextResult
|
|
353
|
+
The other instance.
|
|
354
|
+
|
|
355
|
+
Returns
|
|
356
|
+
-------
|
|
357
|
+
FindTextResult
|
|
358
|
+
A new instance.
|
|
359
|
+
|
|
360
|
+
Raises
|
|
361
|
+
------
|
|
362
|
+
ValueError
|
|
363
|
+
Raised when the two instances have different patterns.
|
|
364
|
+
|
|
365
|
+
"""
|
|
366
|
+
if other.pattern != self.pattern:
|
|
367
|
+
raise ValueError("joined instances must have the same pattern")
|
|
368
|
+
obj = self.__class__(self.pattern)
|
|
369
|
+
obj.extend(self.res + other.res)
|
|
370
|
+
return obj
|
|
371
|
+
|
|
372
|
+
def to_styler(self) -> Styler:
|
|
373
|
+
"""
|
|
374
|
+
Convert `self` to a dataframe `Styler` in convenience of displaying
|
|
375
|
+
in a Jupyter notebook.
|
|
376
|
+
|
|
377
|
+
Returns
|
|
378
|
+
-------
|
|
379
|
+
Styler
|
|
380
|
+
A dataframe `Styler`.
|
|
381
|
+
|
|
382
|
+
"""
|
|
383
|
+
df = pd.DataFrame("", index=range(len(self.res)), columns=["source", "match"])
|
|
384
|
+
for i in range(len(self.res)):
|
|
385
|
+
_tp, _n, _match = self.res[i]
|
|
386
|
+
df.iloc[i, 0] = ".".join(
|
|
387
|
+
[
|
|
388
|
+
"NULL"
|
|
389
|
+
if x.name == "NULL"
|
|
390
|
+
else make_ahref(
|
|
391
|
+
f"{x.relpath}:{x.start_line}:{1+x.spaces}",
|
|
392
|
+
x.name,
|
|
393
|
+
color="inherit",
|
|
394
|
+
)
|
|
395
|
+
for x in _tp.track()
|
|
396
|
+
]
|
|
397
|
+
).replace(".NULL", "")
|
|
398
|
+
df.iloc[i, 0] += ":" + make_ahref(
|
|
399
|
+
f"{_tp.relpath}:{_n}", str(_n), color="inherit"
|
|
400
|
+
)
|
|
401
|
+
df.iloc[i, 1] = re.sub(
|
|
402
|
+
self.pattern,
|
|
403
|
+
lambda x: ""
|
|
404
|
+
if x.group() == ""
|
|
405
|
+
else make_ahref(
|
|
406
|
+
f"{_tp.relpath}:{_n}:{1+_tp.spaces+x.span()[0]}",
|
|
407
|
+
x.group(),
|
|
408
|
+
color="#cccccc",
|
|
409
|
+
background_color="#595959",
|
|
410
|
+
),
|
|
411
|
+
_match,
|
|
412
|
+
)
|
|
413
|
+
return (
|
|
414
|
+
df.style.hide(axis=0)
|
|
415
|
+
.set_properties(**{"text-align": "left"})
|
|
416
|
+
.set_table_styles([dict(selector="th", props=[("text-align", "center")])])
|
|
417
|
+
)
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
def make_ahref(
|
|
421
|
+
url: str,
|
|
422
|
+
display: str,
|
|
423
|
+
color: Union[str, None] = None,
|
|
424
|
+
background_color: Union[str, None] = None,
|
|
425
|
+
) -> str:
|
|
426
|
+
"""
|
|
427
|
+
Makes an HTML <a> tag.
|
|
428
|
+
|
|
429
|
+
Parameters
|
|
430
|
+
----------
|
|
431
|
+
url : str
|
|
432
|
+
URL to link.
|
|
433
|
+
display : str
|
|
434
|
+
Word to display.
|
|
435
|
+
color : Union[str, None], optional
|
|
436
|
+
Text color, by default None.
|
|
437
|
+
background_color : Union[str, None], optional
|
|
438
|
+
Background color, by default None.
|
|
439
|
+
|
|
440
|
+
Returns
|
|
441
|
+
-------
|
|
442
|
+
str
|
|
443
|
+
An HTML <a> tag.
|
|
444
|
+
|
|
445
|
+
"""
|
|
446
|
+
style_list = ["text-decoration:none"]
|
|
447
|
+
if color is not None:
|
|
448
|
+
style_list.append(f"color:{color}")
|
|
449
|
+
if background_color is not None:
|
|
450
|
+
style_list.append(f"background-color:{background_color}")
|
|
451
|
+
style = ";".join(style_list)
|
|
452
|
+
return f"<a href='{url}' style='{style}'>{display}</a>"
|
textpy/core.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import *
|
|
3
|
+
|
|
4
|
+
from .abc import PyText
|
|
5
|
+
from .element import PyFile, PyModule
|
|
6
|
+
|
|
7
|
+
__all__ = ["textpy"]
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def textpy(path_or_text: Union[Path, str]) -> PyText:
|
|
11
|
+
"""
|
|
12
|
+
Statically analyzes a python file or a python module. Each python
|
|
13
|
+
file is recommended to be formatted with `black` and `Auto Docstring
|
|
14
|
+
(numpy format)`, otherwise unexpected errors may occur.
|
|
15
|
+
|
|
16
|
+
Parameters
|
|
17
|
+
----------
|
|
18
|
+
path_or_text : Union[Path, str]
|
|
19
|
+
File path, module path or file text.
|
|
20
|
+
|
|
21
|
+
Returns
|
|
22
|
+
-------
|
|
23
|
+
TextPy
|
|
24
|
+
A class written for python code analysis.
|
|
25
|
+
|
|
26
|
+
Raises
|
|
27
|
+
------
|
|
28
|
+
ValueError
|
|
29
|
+
Raised when `path` is not found.
|
|
30
|
+
|
|
31
|
+
See Also
|
|
32
|
+
--------
|
|
33
|
+
PyModule : Corresponds to a python module.
|
|
34
|
+
PyFile : Contains the text of a python file.
|
|
35
|
+
PyClass : Contains the text of a class and its docstring.
|
|
36
|
+
PyMethod : Contains the text of a class method and its docstring.
|
|
37
|
+
PyFunc : Contains the text of a function and its docstring.
|
|
38
|
+
NumpyFormatDocstring : Stores a numpy-formatted docstring.
|
|
39
|
+
|
|
40
|
+
"""
|
|
41
|
+
if len(path_or_text) < 256 and Path(path_or_text).exists():
|
|
42
|
+
path_or_text = Path(path_or_text)
|
|
43
|
+
if isinstance(path_or_text, str) or path_or_text.is_file():
|
|
44
|
+
return PyFile(path_or_text)
|
|
45
|
+
elif path_or_text.is_dir():
|
|
46
|
+
return PyModule(path_or_text)
|
|
47
|
+
else:
|
|
48
|
+
raise ValueError(f"path not exists: {path_or_text}")
|
textpy/element.py
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from functools import cached_property
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import *
|
|
5
|
+
|
|
6
|
+
from .abc import Docstring, PyText
|
|
7
|
+
from .format import NumpyFormatDocstring
|
|
8
|
+
from .utils.re_extended import line_count_iter, rsplit
|
|
9
|
+
|
|
10
|
+
__all__ = ["PyModule", "PyFile", "PyClass", "PyFunc", "PyMethod"]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class PyModule(PyText):
|
|
14
|
+
def __init__(self, path: Union[Path, str], parent: Union[PyText, None] = None):
|
|
15
|
+
"""
|
|
16
|
+
A python module including multiple python files.
|
|
17
|
+
|
|
18
|
+
Parameters
|
|
19
|
+
----------
|
|
20
|
+
path : Union[Path, str]
|
|
21
|
+
File path.
|
|
22
|
+
parent : Union[TextPy, None], optional
|
|
23
|
+
Parent node (if exists), by default None.
|
|
24
|
+
|
|
25
|
+
Raises
|
|
26
|
+
------
|
|
27
|
+
NotADirectoryError
|
|
28
|
+
Raised when `path` is not a directory.
|
|
29
|
+
|
|
30
|
+
"""
|
|
31
|
+
self.path = Path(path)
|
|
32
|
+
if not self.path.is_dir():
|
|
33
|
+
raise NotADirectoryError(f"not a dicretory: '{self.path}'")
|
|
34
|
+
self.name = self.path.stem
|
|
35
|
+
self.parent = parent
|
|
36
|
+
|
|
37
|
+
@cached_property
|
|
38
|
+
def header(self) -> PyText:
|
|
39
|
+
return PyFile("", parent=self)
|
|
40
|
+
|
|
41
|
+
@cached_property
|
|
42
|
+
def children_dict(self) -> Dict[str, PyText]:
|
|
43
|
+
children_dict: Dict[str, PyText] = {}
|
|
44
|
+
for _path in self.path.iterdir():
|
|
45
|
+
if _path.suffix == ".py":
|
|
46
|
+
children_dict[_path.stem] = PyFile(_path, parent=self)
|
|
47
|
+
elif _path.is_dir():
|
|
48
|
+
_module = PyModule(_path, parent=self)
|
|
49
|
+
if len(_module.children) > 0:
|
|
50
|
+
children_dict[_path.stem] = _module
|
|
51
|
+
return children_dict
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class PyFile(PyText):
|
|
55
|
+
def __init__(
|
|
56
|
+
self,
|
|
57
|
+
path_or_text: Union[Path, str],
|
|
58
|
+
parent: Union[PyText, None] = None,
|
|
59
|
+
start_line: int = 1,
|
|
60
|
+
):
|
|
61
|
+
"""
|
|
62
|
+
Python file.
|
|
63
|
+
|
|
64
|
+
Parameters
|
|
65
|
+
----------
|
|
66
|
+
path_or_text : Union[Path, str]
|
|
67
|
+
File path or file text.
|
|
68
|
+
parent : Union[TextPy, None], optional
|
|
69
|
+
Parent node (if exists), by default None.
|
|
70
|
+
|
|
71
|
+
"""
|
|
72
|
+
if isinstance(path_or_text, Path):
|
|
73
|
+
self.path = path_or_text
|
|
74
|
+
print(self.path)
|
|
75
|
+
self.text = self.path.read_text().strip()
|
|
76
|
+
else:
|
|
77
|
+
self.text = path_or_text.strip()
|
|
78
|
+
|
|
79
|
+
self.name = self.path.stem
|
|
80
|
+
self.parent = parent
|
|
81
|
+
self.start_line = start_line
|
|
82
|
+
self.__header: Union[str, None] = None
|
|
83
|
+
|
|
84
|
+
@cached_property
|
|
85
|
+
def header(self) -> PyText:
|
|
86
|
+
if self.__header is None:
|
|
87
|
+
_ = self.children_dict
|
|
88
|
+
return self.__class__(self.__header, parent=self).as_header()
|
|
89
|
+
|
|
90
|
+
@cached_property
|
|
91
|
+
def children_dict(self) -> Dict[str, PyText]:
|
|
92
|
+
children_dict: Dict[str, PyText] = {}
|
|
93
|
+
_cnt: int = 0
|
|
94
|
+
self.__header = ""
|
|
95
|
+
for i, _text in line_count_iter(rsplit("\n\n\n", self.text)):
|
|
96
|
+
_text = "\n" + _text.strip()
|
|
97
|
+
if re.match("(?:\n@.*)*\ndef ", _text):
|
|
98
|
+
_node = PyFunc(_text, parent=self, start_line=int(i + 3 * (_cnt > 0)))
|
|
99
|
+
children_dict[_node.name] = _node
|
|
100
|
+
elif re.match("(?:\n@.*)*\nclass ", _text):
|
|
101
|
+
_node = PyClass(_text, parent=self, start_line=int(i + 3 * (_cnt > 0)))
|
|
102
|
+
children_dict[_node.name] = _node
|
|
103
|
+
elif _cnt == 0:
|
|
104
|
+
self.__header = _text
|
|
105
|
+
else:
|
|
106
|
+
_node = PyFile(_text, parent=self, start_line=int(i + 3 * (_cnt > 0)))
|
|
107
|
+
children_dict[f"NULL-{i}"] = _node
|
|
108
|
+
_cnt += 1
|
|
109
|
+
return children_dict
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class PyClass(PyText):
|
|
113
|
+
def __init__(
|
|
114
|
+
self, text: str, parent: Union[PyText, None] = None, start_line: int = 1
|
|
115
|
+
):
|
|
116
|
+
"""
|
|
117
|
+
Python class.
|
|
118
|
+
|
|
119
|
+
Parameters
|
|
120
|
+
----------
|
|
121
|
+
text : str
|
|
122
|
+
Class text.
|
|
123
|
+
parent : Union[TextPy, None], optional
|
|
124
|
+
Parent node (if exists), by default None.
|
|
125
|
+
start_line : int, optional
|
|
126
|
+
Starting line number, by default 1.
|
|
127
|
+
|
|
128
|
+
"""
|
|
129
|
+
self.text = text.strip()
|
|
130
|
+
self.name = re.search("class .*?[(:]", self.text).group()[6:-1]
|
|
131
|
+
if self.parent is not None:
|
|
132
|
+
self.path = self.parent.path
|
|
133
|
+
self.parent = parent
|
|
134
|
+
self.start_line = start_line
|
|
135
|
+
self.__header: Union[str, None] = None
|
|
136
|
+
|
|
137
|
+
@cached_property
|
|
138
|
+
def doc(self) -> Docstring:
|
|
139
|
+
if (
|
|
140
|
+
"__init__" in self.children_dict
|
|
141
|
+
and self.children_dict["__init__"].doc.text != ""
|
|
142
|
+
):
|
|
143
|
+
_doc = self.children_dict["__init__"].doc.text
|
|
144
|
+
else:
|
|
145
|
+
_doc = self.header.text
|
|
146
|
+
return NumpyFormatDocstring(_doc, parent=self)
|
|
147
|
+
|
|
148
|
+
@cached_property
|
|
149
|
+
def header(self) -> PyText:
|
|
150
|
+
if self.__header is None:
|
|
151
|
+
_ = self.children_dict
|
|
152
|
+
return self.__class__(
|
|
153
|
+
self.__header, parent=self, start_line=self.start_line
|
|
154
|
+
).as_header()
|
|
155
|
+
|
|
156
|
+
@cached_property
|
|
157
|
+
def children_dict(self) -> Dict[str, PyText]:
|
|
158
|
+
children_dict: Dict[str, PyText] = {}
|
|
159
|
+
sub_text = re.sub("\n ", "\n", self.text)
|
|
160
|
+
_cnt: int = 0
|
|
161
|
+
for i, _str in line_count_iter(rsplit("(?:\n@.*)*\ndef ", sub_text)):
|
|
162
|
+
if _cnt == 0:
|
|
163
|
+
self.__header = _str.replace("\n", "\n ")
|
|
164
|
+
else:
|
|
165
|
+
_node = PyMethod(_str, parent=self, start_line=self.start_line + i)
|
|
166
|
+
children_dict[_node.name] = _node
|
|
167
|
+
_cnt += 1
|
|
168
|
+
return children_dict
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
class PyFunc(PyText):
|
|
172
|
+
def __init__(
|
|
173
|
+
self, text: str, parent: Union[PyText, None] = None, start_line: int = 1
|
|
174
|
+
):
|
|
175
|
+
"""
|
|
176
|
+
Python function.
|
|
177
|
+
|
|
178
|
+
Parameters
|
|
179
|
+
----------
|
|
180
|
+
text : str
|
|
181
|
+
Funtion text.
|
|
182
|
+
parent : Union[TextPy, None], optional
|
|
183
|
+
Parent node (if exists), by default None.
|
|
184
|
+
start_line : int, optional
|
|
185
|
+
Starting line number, by default 1.
|
|
186
|
+
|
|
187
|
+
"""
|
|
188
|
+
self.text = text.strip()
|
|
189
|
+
self.name = re.search("def .*?\(", self.text).group()[4:-1]
|
|
190
|
+
if self.parent is not None:
|
|
191
|
+
self.path = self.parent.path
|
|
192
|
+
self.parent = parent
|
|
193
|
+
self.start_line = start_line
|
|
194
|
+
|
|
195
|
+
@cached_property
|
|
196
|
+
def doc(self) -> Docstring:
|
|
197
|
+
searched = re.search('""".*?"""', self.text, re.DOTALL)
|
|
198
|
+
if searched:
|
|
199
|
+
_doc = re.sub("\n ", "\n", searched.group()[3:-3])
|
|
200
|
+
else:
|
|
201
|
+
_doc = ""
|
|
202
|
+
return NumpyFormatDocstring(_doc, parent=self)
|
|
203
|
+
|
|
204
|
+
@cached_property
|
|
205
|
+
def header(self) -> PyText:
|
|
206
|
+
raise NotImplementedError("`PyFunc.header` not implemented yet")
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
class PyMethod(PyFunc):
|
|
210
|
+
def __init__(
|
|
211
|
+
self, text: str, parent: Union[PyText, None] = None, start_line: int = 1
|
|
212
|
+
):
|
|
213
|
+
"""
|
|
214
|
+
Python class method.
|
|
215
|
+
|
|
216
|
+
Parameters
|
|
217
|
+
----------
|
|
218
|
+
text : str
|
|
219
|
+
Method text.
|
|
220
|
+
parent : Union[TextPy, None], optional
|
|
221
|
+
Parent node (if exists), by default None.
|
|
222
|
+
start_line : int, optional
|
|
223
|
+
Starting line number, by default 1.
|
|
224
|
+
|
|
225
|
+
"""
|
|
226
|
+
super().__init__(text, parent=parent, start_line=start_line)
|
|
227
|
+
self.spaces = 4
|
textpy/format.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from functools import cached_property
|
|
3
|
+
from typing import *
|
|
4
|
+
|
|
5
|
+
from .abc import Docstring
|
|
6
|
+
from .utils.re_extended import rsplit
|
|
7
|
+
|
|
8
|
+
__all__ = ["NumpyFormatDocstring"]
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class NumpyFormatDocstring(Docstring):
|
|
12
|
+
@cached_property
|
|
13
|
+
def sections(self) -> Dict[str, str]:
|
|
14
|
+
details: Dict[str, str] = {}
|
|
15
|
+
for i, _str in enumerate(rsplit(".*\n-+\n", self.text)):
|
|
16
|
+
if i == 0:
|
|
17
|
+
details["_header_"] = _str.strip()
|
|
18
|
+
else:
|
|
19
|
+
_key, _value = re.split("\n-+\n", _str, maxsplit=1)
|
|
20
|
+
details[_key] = _value.strip()
|
|
21
|
+
return details
|
textpy/utils/__init__.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import warnings
|
|
2
|
+
|
|
3
|
+
__all__ = []
|
|
4
|
+
|
|
5
|
+
# import `re_extended` if exists
|
|
6
|
+
try:
|
|
7
|
+
from . import re_extended
|
|
8
|
+
|
|
9
|
+
__all__.extend(re_extended.__all__)
|
|
10
|
+
from .re_extended import *
|
|
11
|
+
|
|
12
|
+
except ImportError as e:
|
|
13
|
+
if isinstance(e, ModuleNotFoundError):
|
|
14
|
+
raise e
|
|
15
|
+
else:
|
|
16
|
+
warnings.warn(e.msg, Warning)
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from typing import *
|
|
3
|
+
|
|
4
|
+
SpanNGroup = Tuple[Tuple[int, int], str]
|
|
5
|
+
LineSpanNGroup = Tuple[int, Tuple[int, int], str]
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"rsplit",
|
|
9
|
+
"lsplit",
|
|
10
|
+
"real_findall",
|
|
11
|
+
"pattern_inreg",
|
|
12
|
+
"line_count",
|
|
13
|
+
"line_count_iter",
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def rsplit(
|
|
18
|
+
pattern: Union[str, re.Pattern],
|
|
19
|
+
string: str,
|
|
20
|
+
maxsplit: int = 0,
|
|
21
|
+
flags: Union[int, re.RegexFlag] = 0,
|
|
22
|
+
) -> List[str]:
|
|
23
|
+
"""
|
|
24
|
+
Split the string by the occurrences of the pattern. Differences to
|
|
25
|
+
`re.split` that the text of all groups in the pattern are also
|
|
26
|
+
returned, each followed by a substring on its right, connected with
|
|
27
|
+
`""`'s.
|
|
28
|
+
|
|
29
|
+
Parameters
|
|
30
|
+
----------
|
|
31
|
+
pattern : Union[str, re.Pattern]
|
|
32
|
+
Pattern string.
|
|
33
|
+
string : str
|
|
34
|
+
String to be splitted.
|
|
35
|
+
maxsplit : int, optional
|
|
36
|
+
Max number of splits, if specified to be 0, there will be no
|
|
37
|
+
more limits, by default 0.
|
|
38
|
+
flags : Union[int, re.RegexFlag], optional
|
|
39
|
+
Regex flag, by default 0.
|
|
40
|
+
|
|
41
|
+
Returns
|
|
42
|
+
-------
|
|
43
|
+
List[str]
|
|
44
|
+
List of substrings.
|
|
45
|
+
|
|
46
|
+
"""
|
|
47
|
+
splits: List[str] = []
|
|
48
|
+
searched = re.search(pattern, string, flags=flags)
|
|
49
|
+
_lstr: str = ""
|
|
50
|
+
while searched:
|
|
51
|
+
_span = searched.span()
|
|
52
|
+
splits.append(_lstr + string[: _span[0]])
|
|
53
|
+
_lstr = searched.group()
|
|
54
|
+
string = string[_span[1] :]
|
|
55
|
+
if maxsplit > 0 and len(splits) >= maxsplit:
|
|
56
|
+
break
|
|
57
|
+
searched = re.search(pattern, string, flags=flags)
|
|
58
|
+
splits.append(_lstr + string)
|
|
59
|
+
return splits
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def lsplit(
|
|
63
|
+
pattern: Union[str, re.Pattern],
|
|
64
|
+
string: str,
|
|
65
|
+
maxsplit: int = 0,
|
|
66
|
+
flags: Union[int, re.RegexFlag] = 0,
|
|
67
|
+
) -> List[str]:
|
|
68
|
+
"""
|
|
69
|
+
Split the string by the occurrences of the pattern. Differences to
|
|
70
|
+
`re.split` that the text of all groups in the pattern are also
|
|
71
|
+
returned, each following a substring on its left, connected with
|
|
72
|
+
`""`'s.
|
|
73
|
+
|
|
74
|
+
Parameters
|
|
75
|
+
----------
|
|
76
|
+
pattern : Union[str, re.Pattern]
|
|
77
|
+
Pattern string.
|
|
78
|
+
string : str
|
|
79
|
+
String to be splitted.
|
|
80
|
+
maxsplit : int, optional
|
|
81
|
+
Max number of splits, if specified to be 0, there will be no
|
|
82
|
+
more limits, by default 0.
|
|
83
|
+
flags : Union[int, re.RegexFlag], optional
|
|
84
|
+
Regex flag, by default 0.
|
|
85
|
+
|
|
86
|
+
Returns
|
|
87
|
+
-------
|
|
88
|
+
List[str]
|
|
89
|
+
List of substrings.
|
|
90
|
+
|
|
91
|
+
"""
|
|
92
|
+
splits: List[str] = []
|
|
93
|
+
searched = re.search(pattern, string, flags=flags)
|
|
94
|
+
while searched:
|
|
95
|
+
_span = searched.span()
|
|
96
|
+
splits.append(string[: _span[1]])
|
|
97
|
+
string = string[_span[1] :]
|
|
98
|
+
if maxsplit > 0 and len(splits) >= maxsplit:
|
|
99
|
+
break
|
|
100
|
+
searched = re.search(pattern, string, flags=flags)
|
|
101
|
+
splits.append(string)
|
|
102
|
+
return splits
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def real_findall(
|
|
106
|
+
pattern: Union[str, re.Pattern],
|
|
107
|
+
string: str,
|
|
108
|
+
flags: Union[int, re.RegexFlag] = 0,
|
|
109
|
+
linemode: bool = False,
|
|
110
|
+
) -> List[Union[SpanNGroup, LineSpanNGroup]]:
|
|
111
|
+
"""
|
|
112
|
+
Finds all non-overlapping matches in the string. Differences to
|
|
113
|
+
`re.findall` that it also returns the spans of patterns.
|
|
114
|
+
|
|
115
|
+
Parameters
|
|
116
|
+
----------
|
|
117
|
+
pattern : Union[str, re.Pattern]
|
|
118
|
+
Pattern string.
|
|
119
|
+
string : str
|
|
120
|
+
String to be searched.
|
|
121
|
+
flags : Union[int, re.RegexFlag], optional
|
|
122
|
+
Regex flag, by default 0.
|
|
123
|
+
linemode : bool, optional
|
|
124
|
+
If true, match the pattern on each line of the string, by
|
|
125
|
+
default False.
|
|
126
|
+
|
|
127
|
+
Returns
|
|
128
|
+
-------
|
|
129
|
+
List[Union[SpanNGroup, LineSpanNGroup]]
|
|
130
|
+
List of finding result. If `linemode` is false, each list
|
|
131
|
+
element consists of the span and the group of the pattern. If
|
|
132
|
+
`linemode` is true, each list element consists of the line
|
|
133
|
+
number, the span (within the line), and the group of the
|
|
134
|
+
pattern instead.
|
|
135
|
+
|
|
136
|
+
"""
|
|
137
|
+
finds: List[SpanNGroup] = []
|
|
138
|
+
_sum: int = 0
|
|
139
|
+
_line: int = 1
|
|
140
|
+
_inline_pos: int = 0
|
|
141
|
+
searched = re.search(pattern, string, flags=flags)
|
|
142
|
+
while searched:
|
|
143
|
+
_len_string = len(string)
|
|
144
|
+
_span, _group = searched.span(), searched.group()
|
|
145
|
+
if linemode:
|
|
146
|
+
_lsting = string[: _span[0]]
|
|
147
|
+
_lline = line_count(_lsting) - 1
|
|
148
|
+
_line += _lline
|
|
149
|
+
if _lline > 0:
|
|
150
|
+
_inline_pos = 0
|
|
151
|
+
_lastline_pos = len(_lsting) - 1 - _lsting.rfind("\n")
|
|
152
|
+
finds.append(
|
|
153
|
+
(
|
|
154
|
+
_line,
|
|
155
|
+
(
|
|
156
|
+
_inline_pos + _lastline_pos,
|
|
157
|
+
_inline_pos + _lastline_pos + _span[1] - _span[0],
|
|
158
|
+
),
|
|
159
|
+
_group,
|
|
160
|
+
)
|
|
161
|
+
)
|
|
162
|
+
_line += line_count(_group) - 1
|
|
163
|
+
if "\n" in _group:
|
|
164
|
+
_inline_pos = len(_group) - 1 - _group.rfind("\n")
|
|
165
|
+
else:
|
|
166
|
+
_inline_pos += max(_lastline_pos + _span[1] - _span[0], 1)
|
|
167
|
+
else:
|
|
168
|
+
finds.append(((_span[0] + _sum, _span[1] + _sum), _group))
|
|
169
|
+
_sum += max(_span[1], 1)
|
|
170
|
+
if _len_string == 0:
|
|
171
|
+
break
|
|
172
|
+
if _span[1] == 0:
|
|
173
|
+
_line += 1 if string[0] == "\n" else 0
|
|
174
|
+
string = string[1:]
|
|
175
|
+
else:
|
|
176
|
+
string = string[_span[1] :]
|
|
177
|
+
searched = re.search(pattern, string, flags=flags) # search again
|
|
178
|
+
return finds
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def pattern_inreg(pattern: str) -> str:
|
|
182
|
+
"""
|
|
183
|
+
Invalidates the regular expressions in `pattern`.
|
|
184
|
+
|
|
185
|
+
Parameters
|
|
186
|
+
----------
|
|
187
|
+
pattern : str
|
|
188
|
+
Pattern to be invalidated.
|
|
189
|
+
|
|
190
|
+
Returns
|
|
191
|
+
-------
|
|
192
|
+
str
|
|
193
|
+
A new pattern.
|
|
194
|
+
|
|
195
|
+
"""
|
|
196
|
+
return re.sub("[$^.\[\]*+-?!{},|:#><=\\\\]", lambda x: "\\" + x.group(), pattern)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def line_count(string: str) -> int:
|
|
200
|
+
"""
|
|
201
|
+
Counts the number of lines in a string.
|
|
202
|
+
|
|
203
|
+
Parameters
|
|
204
|
+
----------
|
|
205
|
+
string : str
|
|
206
|
+
A string.
|
|
207
|
+
|
|
208
|
+
Returns
|
|
209
|
+
-------
|
|
210
|
+
int
|
|
211
|
+
Total number of lines.
|
|
212
|
+
|
|
213
|
+
"""
|
|
214
|
+
return 1 + len(re.findall("\n", string))
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def line_count_iter(iter: Iterable[str]) -> Iterable[Tuple[int, str]]:
|
|
218
|
+
"""
|
|
219
|
+
Counts the number of lines in each string, and returns the cumsumed
|
|
220
|
+
values.
|
|
221
|
+
|
|
222
|
+
Parameters
|
|
223
|
+
----------
|
|
224
|
+
iter : Iterable[str]
|
|
225
|
+
An iterable of strings.
|
|
226
|
+
|
|
227
|
+
Yields
|
|
228
|
+
------
|
|
229
|
+
Tuple[int, str]
|
|
230
|
+
Each time, yields the cumsumed number of lines til now together
|
|
231
|
+
with a string found in `iter`, until `iter` is traversed.
|
|
232
|
+
|
|
233
|
+
"""
|
|
234
|
+
_cnt = 1
|
|
235
|
+
for _str in iter:
|
|
236
|
+
yield _cnt, _str
|
|
237
|
+
_cnt += len(re.findall("\n", _str))
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
BSD 2-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023, Chitaoji
|
|
4
|
+
|
|
5
|
+
Redistribution and use in source and binary forms, with or without
|
|
6
|
+
modification, are permitted provided that the following conditions are met:
|
|
7
|
+
|
|
8
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
9
|
+
list of conditions and the following disclaimer.
|
|
10
|
+
|
|
11
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
12
|
+
this list of conditions and the following disclaimer in the documentation
|
|
13
|
+
and/or other materials provided with the distribution.
|
|
14
|
+
|
|
15
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
16
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
17
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
18
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
19
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
20
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
21
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
22
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
23
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
24
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: textpy
|
|
3
|
+
Version: 0.0.0
|
|
4
|
+
Summary: Reads a python file/module and statically analyzes it.
|
|
5
|
+
Home-page: https://github.com/Chitaoji/textpy
|
|
6
|
+
Author: Chitaoji
|
|
7
|
+
Author-email: 2360742040@qq.com
|
|
8
|
+
License: BSD
|
|
9
|
+
Classifier: License :: OSI Approved :: BSD License
|
|
10
|
+
Classifier: Programming Language :: Python
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
13
|
+
Requires-Python: >=3.8.13
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
License-File: LICENSE
|
|
16
|
+
Requires-Dist: attrs
|
|
17
|
+
Requires-Dist: pandas
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# textpy
|
|
21
|
+
Reads a python file/module and statically analyzes it.
|
|
22
|
+
|
|
23
|
+
## Installation
|
|
24
|
+
|
|
25
|
+
```sh
|
|
26
|
+
pip install textpy
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Examples
|
|
30
|
+
Create a new file named `this_is_a_file.py`:
|
|
31
|
+
|
|
32
|
+
```py
|
|
33
|
+
class ThisIsAClass:
|
|
34
|
+
def __init__(self):
|
|
35
|
+
"""Write something."""
|
|
36
|
+
self.var_1 = "hahaha"
|
|
37
|
+
self.var_2 = "blabla"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def this_is_a_function(a: ThisIsAClass):
|
|
41
|
+
"""
|
|
42
|
+
Write something.
|
|
43
|
+
|
|
44
|
+
Parameters
|
|
45
|
+
----------
|
|
46
|
+
a : ThisIsAClass
|
|
47
|
+
An object.
|
|
48
|
+
|
|
49
|
+
"""
|
|
50
|
+
print(a.var_1, a.var_2)
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Run the following codes to find all the occurrences of the pattern `"var"` in `this_is_a_file.py`:
|
|
54
|
+
|
|
55
|
+
```py
|
|
56
|
+
from textpy import textpy
|
|
57
|
+
|
|
58
|
+
res = textpy("this_is_a_file.py").findall("var", styler=False)
|
|
59
|
+
print(res)
|
|
60
|
+
# Output:
|
|
61
|
+
# this_is_a_file.py:4: ' self.var_1 = "hahaha"'
|
|
62
|
+
# this_is_a_file.py:5: ' self.var_2 = "blabla"'
|
|
63
|
+
# this_is_a_file.py:18: ' print(a.var_1, a.var_2)'
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Also, when using a Jupyter notebook, you can run a cell like this:
|
|
67
|
+
|
|
68
|
+
```py
|
|
69
|
+
from textpy import textpy
|
|
70
|
+
|
|
71
|
+
textpy("this_is_a_file.py").findall("var")
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
and the output will be like:
|
|
75
|
+
|
|
76
|
+

|
|
77
|
+
|
|
78
|
+
Now suppose you've got a python module consists of a few files, for example, our `textpy` module itself, you can do almost the same thing:
|
|
79
|
+
|
|
80
|
+
```py
|
|
81
|
+
module_path = "textpy/" # you can type any path here
|
|
82
|
+
pattern = "note.*k" # type any regular expression here
|
|
83
|
+
|
|
84
|
+
res = textpy(module_path).findall(pattern, styler=False)
|
|
85
|
+
print(res)
|
|
86
|
+
# Output:
|
|
87
|
+
# textpy_local/textpy/abc.py:158: ' in a Jupyter notebook, by default True.'
|
|
88
|
+
# textpy_local/textpy/abc.py:375: ' in a Jupyter notebook.'
|
|
89
|
+
```
|
|
90
|
+
## License
|
|
91
|
+
This project falls under the BSD 2-Clause License.
|
|
92
|
+
|
|
93
|
+
## v0.1.3
|
|
94
|
+
* Initial release.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
textpy/__init__.py,sha256=5oLjKa8mwM03dH5jc7526vt866trItW03Mu1azOypK0,1030
|
|
2
|
+
textpy/__version__.py,sha256=O9-8j_dDi79rAllM4J_l2fbsq4NWiGnQhlf91GOgm9o,63
|
|
3
|
+
textpy/abc.py,sha256=Y1bqElEbQu3JM2V5gSNnVolsGTDXVQ-2-Kc18WyqVw8,11748
|
|
4
|
+
textpy/core.py,sha256=WXUi2ubSw38UOUFRUu7cvQwL8IuKYCLwIj2DnB_EIx0,1413
|
|
5
|
+
textpy/element.py,sha256=uqqj3gb_L1iD51F7gCrWk56tAhp7dgw0W0ekEibakK4,7061
|
|
6
|
+
textpy/format.py,sha256=SCgJ8vjdYsOR30xZUdLTnUjIj-JqLNTf13Lqy0BlOa8,607
|
|
7
|
+
textpy/utils/__init__.py,sha256=77LZdb0LWdqR8GaHo6mmubcGYHCsrwRnith6adkh0Wg,304
|
|
8
|
+
textpy/utils/re_extended.py,sha256=nespS2veOa7Y5akpn1qOWjh_e7b8DEAjOkCfh2PBpaE,6385
|
|
9
|
+
textpy-0.0.0.dist-info/LICENSE,sha256=2mvoSaHTcgpKXSRr7Q1UeqdKB9H670-BUBH5koBypks,1297
|
|
10
|
+
textpy-0.0.0.dist-info/METADATA,sha256=cp5Et1hZVVZnR9neWqAnOsxJNOKFvUlMqT7uRd9fw9Y,2227
|
|
11
|
+
textpy-0.0.0.dist-info/WHEEL,sha256=iYlv5fX357PQyRT2o6tw1bN-YcKFFHKqB_LwHO5wP-g,110
|
|
12
|
+
textpy-0.0.0.dist-info/top_level.txt,sha256=oWHnPcR9GIzbwkj6ZR9wflWbnft6QtEx8r6MbNbSBR4,7
|
|
13
|
+
textpy-0.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
textpy
|