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 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
@@ -0,0 +1,3 @@
1
+ VERSION = (0, 0, 0)
2
+
3
+ __version__ = ".".join(map(str, VERSION))
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
@@ -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
+ ![Alt text](images/example_1.png)
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,6 @@
1
+ Wheel-Version: 1.0
2
+ Generator: bdist_wheel (0.41.2)
3
+ Root-Is-Purelib: true
4
+ Tag: py2-none-any
5
+ Tag: py3-none-any
6
+
@@ -0,0 +1 @@
1
+ textpy