Skip to content

Commit 55f3c63

Browse files
authored
Merge pull request #78 from ionite34/dev
2 parents f0fee45 + a3d2cf6 commit 55f3c63

File tree

14 files changed

+603
-195
lines changed

14 files changed

+603
-195
lines changed

docs/source/conf.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
project = "einspect"
1616
copyright = "2023, Ionite"
1717
author = "Ionite"
18-
release = "v0.5.13"
18+
release = "v0.5.15"
1919

2020

2121
# -- General configuration ---------------------------------------------------

poetry.lock

Lines changed: 168 additions & 167 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "einspect"
3-
version = "0.5.13"
3+
version = "0.5.15"
44
packages = [{ include = "einspect", from = "src" }]
55
description = "Extended Inspect - view and modify memory structs of runtime objects."
66
authors = ["ionite34 <dev@ionite.io>"]
@@ -42,7 +42,7 @@ python = ">=3.8,<3.13"
4242
typing-extensions = "^4.4.0"
4343

4444
[tool.poetry.group.dev.dependencies]
45-
pytest = "^7.2.0"
45+
pytest = "^7.2.2"
4646
pytest-cov = "^4.0.0"
4747
pytest-xdist = "^3.1.0"
4848
mypy = "^0.991"
@@ -62,11 +62,11 @@ jupyter = "^1.0.0"
6262

6363
[tool.poetry.group.ci.dependencies]
6464
typing-extensions = "^4.4.0"
65-
pytest = "^7.2.0"
65+
pytest = "^7.2.2"
6666

6767
[tool.poetry.group.ci-cov.dependencies]
6868
typing-extensions = "^4.4.0"
69-
pytest = "^7.2.0"
69+
pytest = "^7.2.2"
7070
pytest-cov = "^4.0.0"
7171

7272
[tool.poetry.group.docs.dependencies]

src/einspect/__init__.py

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,10 @@
77
from einspect.views.unsafe import global_unsafe
88
from einspect.views.view_type import impl
99

10-
# Runtime patches
11-
einspect._patch.run()
12-
13-
__all__ = ("view", "unsafe", "impl", "orig", "ptr", "NULL")
14-
15-
__version__ = "0.5.13"
10+
__all__ = ("view", "unsafe", "impl", "orig", "ptr", "NULL", "__version__")
11+
__version__ = "0.5.15"
1612

1713
unsafe = global_unsafe
14+
15+
# Runtime patches
16+
einspect._patch.run()

src/einspect/structs/py_dict.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,10 @@ class PyDictKeysObject(Struct):
3636
Defines a DictKeysObject Structure.
3737
3838
https://github.com/python/cpython/blob/3.11/Include/internal/pycore_dict.h#L87-L125
39+
40+
..
41+
source: Include/internal/pycore_dict.h (struct _dictkeysobject) #[59827ad2d5]
42+
source[<3.11]: Objects/dict-common.h (struct _dictkeysobject) #[12e0897a69]
3943
"""
4044

4145
dk_refcnt: int

src/einspect/structs/py_list.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ class PyListObject(PyVarObject[list, None, _VT], IsGC):
1818
Defines a PyListObject Structure.
1919
2020
https://github.com/python/cpython/blob/3.11/Include/cpython/listobject.h
21+
22+
..
23+
source: Include/cpython/listobject.h (struct PyListObject) #[15e5541daa]
24+
source[<3.9]: Include/listobject.h (struct PyListObject) #[15e5541daa]
2125
"""
2226

2327
ob_item: ptr[ptr[PyObject[_VT, None, None]]]

src/einspect/structs/py_object.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,15 @@ def py_set(obj_ptr: ptr[PyObject], value: object | PyObject | ptr[PyObject]) ->
6767

6868

6969
class PyObject(Struct, AsRef, Generic[_T, _KT, _VT]):
70-
"""Defines a base PyObject Structure."""
70+
"""
71+
Defines a base PyObject Structure.
72+
73+
..
74+
source: Include/object.h (struct _object)
75+
source[=3.8]: #[cc8cfe0d6c]
76+
source[<3.11]: #[c39f0de871]
77+
source[<3.13]: #[79c692b04c]
78+
"""
7179

7280
ob_refcnt: int
7381
ob_type: Annotated[ptr[PyTypeObject[Type[_T]]], c_void_p]

src/einspect/structs/py_set.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@ class PySetObject(PyObject[set, None, _T], IsGC):
3333
Defines a PySetObject Structure.
3434
3535
https://github.com/python/cpython/blob/3.11/Include/cpython/setobject.h#L36-L59
36+
37+
..
38+
source: Include/cpython/setobject.h (struct PySetObject) #[5a21a4c7be]
39+
source[<3.11]: Include/setobject.h (struct PySetObject) #[5a21a4c7be]
3640
"""
3741

3842
fill: int # Number active and dummy entries

src/einspect/structs/py_tuple.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ class PyTupleObject(PyVarObject[tuple, None, _VT], IsGC):
1919
Defines a PyTupleObject Structure.
2020
2121
https://github.com/python/cpython/blob/3.11/Include/cpython/tupleobject.h
22+
23+
..
24+
source: Include/cpython/tupleobject.h (struct PyTupleObject) #[58f9cac944]
2225
"""
2326

2427
# Size of this array is only known after creation

src/einspect/structs/py_type.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@ class PyTypeObject(PyVarObject[_T, None, None]):
3838
Defines a PyTypeObject Structure.
3939
4040
https://github.com/python/cpython/blob/3.11/Doc/includes/typestruct.h
41+
42+
..
43+
source: Include/cpython/object.h (struct _typeobject)
4144
"""
4245

4346
tp_name: Annotated[bytes, c_char_p]
@@ -171,11 +174,12 @@ def setattr_safe(self, name: str, value: Any) -> None:
171174
field_type = field[1]
172175
# For c_char_p types, set encoded bytes
173176
if field_type is c_char_p:
174-
setattr(self, slot.name, value.encode("utf-8"))
177+
return setattr(self, slot.name, value.encode("utf-8"))
175178
# For ptr[PyObject] types, set PyObject pointer
176179
elif field_type == POINTER(PyObject):
177-
setattr(self, slot.name, PyObject.from_object(value).as_ref())
180+
return setattr(self, slot.name, PyObject.from_object(value).as_ref())
178181

182+
# If not a recognized slot, set with PyObject_SetAttr api
179183
self.SetAttr(name, value)
180184

181185
def _try_del_tp_dict(self, name: str) -> None:

tests/test_impl.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,3 +99,22 @@ def real(self):
9999
# Restore original
100100
view(int).restore("real")
101101
assert int.real is not real
102+
103+
104+
def test_impl_view():
105+
v = view(frozenset)
106+
v["__name__"] = "custom_frozenset"
107+
v["__getitem__"] = lambda self, item: len(self) * item
108+
109+
assert frozenset.__name__ == "custom_frozenset"
110+
assert repr(frozenset) == "<class 'custom_frozenset'>"
111+
112+
# noinspection PyTypeChecker,PyUnresolvedReferences
113+
res = frozenset({1, 2})[5]
114+
assert res == 10
115+
116+
v.restore("__name__", "__getitem__")
117+
118+
with pytest.raises(TypeError):
119+
# noinspection PyTypeChecker,PyUnresolvedReferences
120+
_ = frozenset({1, 2})[5]

tools/app.py

Lines changed: 114 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,133 @@
11
import typer
2-
from rich import Console
2+
from rich.columns import Columns
3+
from rich.console import Console, ConsoleRenderable, Group
4+
from rich.panel import Panel
5+
from rich.style import Style
36
from rich.syntax import Syntax
7+
from rich.tree import Tree
48

5-
from tools.sources import branch_range, extract_struct, fetch_git
9+
from tools.sources import (
10+
CPYTHON_MAIN_VERSION,
11+
SourceBlock,
12+
branch_range,
13+
build_link,
14+
check_hash,
15+
extract_struct,
16+
fetch_git,
17+
hash_source,
18+
)
19+
from tools.struct_meta import StructMetadata
620

721
console = Console()
22+
cp = console.print
823
app = typer.Typer()
924

25+
PANEL_STYLE = Style(color="grey0")
26+
27+
28+
def make_struct_tree(title: str, panel: Panel) -> Tree:
29+
"""Create a tree with a panel as a leaf."""
30+
tree = Tree(title)
31+
tree.add(panel)
32+
return tree
33+
34+
35+
def make_panel_code(heading: str, body: dict[str, str], code: Syntax | str) -> Panel:
36+
"""Create a panel with a heading and a table of field titles."""
37+
body_entries = (f"[bold]{title}[/bold]: {value}" for title, value in body.items())
38+
column = Columns(body_entries, equal=True)
39+
return Panel(
40+
Group(column, code), title=heading, title_align="left", border_style=PANEL_STYLE
41+
)
42+
43+
44+
def make_panel_msg(heading: str, msg: str | ConsoleRenderable) -> Panel:
45+
"""Create a panel with a heading and a message."""
46+
return Panel(
47+
msg,
48+
title=heading,
49+
)
50+
1051

1152
@app.command()
1253
def compare_src(path: str, struct: str, min_tag: str = "3.8", max_tag: str = "main"):
1354
for branch in branch_range(min_tag, max_tag):
1455
text = fetch_git(path, branch)
15-
struct_text = extract_struct(struct, text)
16-
struct_fmt = Syntax(struct_text, "c", line_numbers=True)
17-
console.print(f"[bold blue]{branch}[/bold blue]")
18-
console.print(struct_fmt)
56+
st = extract_struct(struct, text)
57+
cp(f"[bold blue]{branch}[/bold blue]")
58+
cp(f"[gray]{st.code_hash()}[/gray]")
59+
cp(st.as_syntax())
1960

2061

2162
@app.command()
2263
def compare_meta(struct_name: str, min_tag: str = "3.8", max_tag: str = "main"):
2364
"""Compare an einspect Struct with CPython source. Requires docstring metadata."""
65+
metadata = StructMetadata.from_struct_name(struct_name)
2466

67+
# Title
68+
cls_name = metadata.source_cls.__qualname__
69+
module_name = metadata.source_cls.__module__
70+
title = f"[grey0]{module_name}.[/grey0][bold cyan]{cls_name}[/bold cyan]"
71+
cp(title)
72+
73+
last_hash: str | None = None
74+
last_code: SourceBlock | None = None
2575
for branch in branch_range(min_tag, max_tag):
26-
text = fetch_git(path, branch)
27-
console.print(f"[bold blue]{branch}[/bold blue]")
28-
console.print(text)
76+
if branch == "main":
77+
version = CPYTHON_MAIN_VERSION
78+
else:
79+
version = tuple(int(v) for v in branch.split("."))
80+
81+
# Build panel
82+
title = f"[bold blue]{branch}[/bold blue]"
83+
body: dict[str, str] = {}
84+
85+
try:
86+
entry = metadata.get_version(version)
87+
except KeyError as e:
88+
cp(make_panel_msg(title, f"[bold red]{e}[/bold red]"))
89+
continue
90+
91+
git_text = fetch_git(entry.file, branch)
92+
if entry.def_kind == "struct":
93+
try:
94+
st = extract_struct(entry.def_name, git_text)
95+
except ValueError as e:
96+
link = build_link(entry.file, branch)
97+
raise ValueError(
98+
f"Struct {entry.def_name!r} not found in {branch!r} branch {link}"
99+
) from e
100+
101+
# Hashes
102+
remote_hash = st.code_hash()
103+
body["hash"] = f"[plum4]{remote_hash}[/plum4]"
104+
if local_hash := entry.hash:
105+
if not check_hash(local_hash):
106+
body[
107+
"hash (local)"
108+
] = f"[bold red]{local_hash}[/bold red] [red](invalid)[/red]"
109+
elif remote_hash != local_hash:
110+
body[
111+
"hash (local)"
112+
] = f"[bold yellow]{local_hash}[/bold yellow] [red](does not match)[/red]"
113+
else:
114+
body["hash (local)"] = f"[green]{local_hash}[/green]"
115+
116+
if last_hash == remote_hash:
117+
code = "[grey0]No changes[/grey0]"
118+
else:
119+
last_hash = remote_hash
120+
st = st.normalize()
121+
code = st.as_syntax(line_numbers=True)
122+
123+
# Add diff
124+
if last_code:
125+
code = st.diff(last_code).as_syntax(line_numbers=True)
126+
127+
last_code = st
128+
129+
panel = make_panel_code(title, body, code)
130+
cp(panel)
131+
132+
else:
133+
raise ValueError(f"Unsupported definition kind: {entry.def_kind}")

0 commit comments

Comments
 (0)