Skip to content

Commit

Permalink
Merge pull request #33 from ionite34/dev
Browse files Browse the repository at this point in the history
  • Loading branch information
ionite34 committed Jan 24, 2023
2 parents bbac5e7 + 5b48362 commit cd2422d
Show file tree
Hide file tree
Showing 20 changed files with 326 additions and 94 deletions.
3 changes: 3 additions & 0 deletions .github/workflows/coverage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ jobs:
poetry install --only ci-cov
- name: Run Tests
env:
PYTHONDEVMODE: 1
PYTHONMALLOC: "debug"
run: |
poetry run pytest --cov=./src --cov-report=term --cov-report=xml
Expand Down
2 changes: 1 addition & 1 deletion docs/source/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
project = "einspect"
copyright = "2023, Ionite"
author = "Ionite"
release = "v0.4.10"
release = "v0.5.0a1"


# -- General configuration ---------------------------------------------------
Expand Down
4 changes: 3 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "einspect"
version = "0.4.10"
version = "0.5.0a1"
packages = [{ include = "einspect", from = "src" }]
description = "Extended Inspect - view and modify memory structs of runtime objects."
authors = ["ionite34 <dev@ionite.io>"]
Expand Down Expand Up @@ -75,6 +75,7 @@ exclude_lines = [
"pragma: no cover",
"@overload",
"@abstractmethod",
"log.debug",
]

[tool.isort]
Expand All @@ -84,6 +85,7 @@ skip = ["einspect/structs/__init__.py"]
markers = [
"run_in_subprocess: Run marked test in a subprocess",
]
filterwarnings = ["error"]

[tool.mypy]
python_version = 3.11
Expand Down
2 changes: 1 addition & 1 deletion src/einspect/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,6 @@

__all__ = ("view", "unsafe", "impl", "orig")

__version__ = "0.4.10"
__version__ = "0.5.0a1"

unsafe: ContextManager[None] = global_unsafe
5 changes: 2 additions & 3 deletions src/einspect/protocols/delayed_bind.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,10 @@ def __init__(self, py_api: FuncPtr, func: _F):
def __repr__(self):
return f"<{self.__class__.__name__} property {self.attrname!r}>"

def __set_name__(self, owner, name):
def __set_name__(self, owner: type, name: str) -> None:
if self.attrname is None:
self.attrname = name
elif name != self.attrname:
elif name != self.attrname: # pragma: no cover
raise TypeError(
"Cannot assign the same bind to two different names "
f"({self.attrname!r} and {name!r})."
Expand Down Expand Up @@ -91,7 +91,6 @@ def _get_defining_type_hints(self, owner_cls: type) -> tuple[list[type], type]:
def_cls = _get_defining_class_of_bound_method(self.func, owner_cls)
log.debug("Found defining class: %s of %s", def_cls, self.func)
arg_t.insert(0, def_cls)
# arg_t.insert(0, owner_cls)

arg_t = [convert_type_hints(t, owner_cls) for t in arg_t]

Expand Down
2 changes: 2 additions & 0 deletions src/einspect/structs/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from einspect.structs.py_type import PyTypeObject
from einspect.structs.py_long import PyLongObject # Req before PyBoolObject
from einspect.structs.py_bool import PyBoolObject
from einspect.structs.py_float import PyFloatObject
from einspect.structs.py_unicode import (
PyUnicodeObject,
PyCompactUnicodeObject,
Expand All @@ -25,6 +26,7 @@
"PyListObject",
"PyLongObject",
"PyBoolObject",
"PyFloatObject",
"PyUnicodeObject",
"PyCompactUnicodeObject",
"PyASCIIObject",
Expand Down
26 changes: 22 additions & 4 deletions src/einspect/structs/py_type.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,12 @@ class PyTypeObject(PyVarObject[_T, None, None]):
# bitset of which type-watchers care about this type
tp_watched: c_char

def __repr__(self) -> str:
"""Return a string representation of the PyTypeObject."""
cls_name = f"{self.__class__.__name__}"
type_name = self.tp_name.decode()
return f"<{cls_name}[{type_name}] at {self.address:#04x}>"

@classmethod
def from_object(cls, obj: Type[_T]) -> PyTypeObject[Type[_T]]:
return super().from_object(obj) # type: ignore
Expand All @@ -138,10 +144,6 @@ def setattr_safe(self, name: str, value: Any) -> None:
return
self.SetAttr(name, value)

@bind_api(pythonapi["PyType_Modified"])
def Modified(self) -> None:
"""Mark the type as modified."""

def is_gc(self) -> bool:
"""
Return True if the type has GC support.
Expand All @@ -151,6 +153,22 @@ def is_gc(self) -> bool:
"""
return bool(self.tp_flags & TpFlags.HAVE_GC)

@bind_api(pythonapi["PyType_Modified"])
def Modified(self) -> None:
"""Mark the type as modified."""

@bind_api(pythonapi["_PyObject_NewVar"])
def NewVar(self, nitems: int) -> ptr[PyVarObject]:
"""Create a new variable object of the type."""

@bind_api(pythonapi["_PyObject_New"])
def New(self) -> ptr[PyObject]:
"""Create a new object of the type."""

@bind_api(pythonapi["_PyObject_GC_NewVar"])
def GC_NewVar(self, nitems: int) -> ptr[PyVarObject]:
"""Create a new variable object of the type with GC support."""


# Mapping of CField name to type
# noinspection PyProtectedMember
Expand Down
19 changes: 8 additions & 11 deletions src/einspect/views/factory.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
"""Function factory to create views for objects."""
from __future__ import annotations

import warnings
from types import MappingProxyType
from typing import Any, Final, TypeVar, overload

Expand Down Expand Up @@ -114,15 +113,13 @@ def view(obj, ref: bool = REF_DEFAULT):
A view onto the object.
"""
obj_type = type(obj)

if obj_type in VIEW_TYPES:
return VIEW_TYPES[obj_type](obj, ref=ref)
else:
res = View(obj, ref=ref)
msg = (
"Using `einspect.view` on objects without"
" a concrete View subclass will be deprecated."
" Use `einspect.views.AnyView` instead."
)
warnings.warn(msg, DeprecationWarning, stacklevel=2)
return res

# Fallback to subclasses
for obj_type, view_type in reversed([*VIEW_TYPES.items()]):
if isinstance(obj, obj_type):
return view_type(obj, ref=ref)

# Shouldn't reach here since we will at least match isinstance object
raise TypeError(f"Cannot create view for {obj_type}") # pragma: no cover
55 changes: 55 additions & 0 deletions src/einspect/views/moves.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
from __future__ import annotations

from ctypes import addressof
from typing import TYPE_CHECKING

from einspect.api import PTR_SIZE
from einspect.errors import UnsafeError

if TYPE_CHECKING:
from einspect.views.view_base import View


def _check_move(dst: View, src: View) -> None:
"""
Check if a memory from `dst` to `src` is safe.
Raises:
UnsafeError: If the move is unsafe.
"""
# If non-gc type into gc type, always unsafe
if not src.is_gc() and dst.is_gc():
raise UnsafeError(
f"Move of non-gc type {src.type.__name__!r} into gc type"
f" {dst.type.__name__!r} requires an unsafe context."
)

dst_allocated = dst.mem_allocated
dst_offset = 0

# Check if dst has an instance dict
if (dst_dict := dst._pyobject.instance_dict()) is not None:
# If offset is positive, add this to dst_allocated
dst_offset = addressof(dst_dict) - dst._pyobject.address
if dst_offset > 0:
dst_allocated = max((dst_offset + PTR_SIZE), dst_allocated)

# Check if we need to move an instance dict
if (src_dict := src._pyobject.instance_dict()) is not None:
src_offset = addressof(src_dict) - src._pyobject.address
# If negative, it must match dst_offset
neg_unsafe = src_offset < 0 and src_offset != dst_offset
# Otherwise, needs to be within mem_allocated
pos_unsafe = src_offset > 0 and (src_offset + PTR_SIZE) > dst_allocated
if neg_unsafe or pos_unsafe:
raise UnsafeError(
f"memory move of instance dict at offset {src_offset} from {src.type.__name__!r} "
f"to {dst.type.__name__!r} is out of bounds. Enter an unsafe context to allow this."
)

# Check if src mem_size can fit into dst mem_allocated
if src.mem_size > dst_allocated:
raise UnsafeError(
f"memory move of {src.mem_size} bytes into allocated space of {dst.mem_allocated} bytes"
" is out of bounds. Enter an unsafe context to allow this."
)
95 changes: 67 additions & 28 deletions src/einspect/views/view_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,21 @@
import warnings
import weakref
from abc import ABC
from contextlib import ExitStack
from copy import deepcopy
from contextlib import suppress
from ctypes import py_object
from functools import cached_property
from typing import Any, Final, Generic, Type, TypeVar, get_args, get_type_hints
from typing import (
Any,
Final,
Generic,
Type,
TypeVar,
get_args,
get_type_hints,
overload,
)

from einspect.api import Py, PyObj_FromPtr, align_size
from einspect.api import PTR_SIZE, Py, PyObj_FromPtr, align_size
from einspect.errors import (
DroppedReference,
MovedError,
Expand All @@ -20,6 +28,7 @@
)
from einspect.structs import PyObject, PyTypeObject, PyVarObject
from einspect.views._display import Formatter
from einspect.views.moves import _check_move
from einspect.views.unsafe import UnsafeContext, unsafe

__all__ = ("View", "VarView", "AnyView", "REF_DEFAULT")
Expand All @@ -31,7 +40,10 @@
_T = TypeVar("_T")
_KT = TypeVar("_KT")
_VT = TypeVar("_VT")
_V = TypeVar("_V", bound="View")

# For moves
_View = TypeVar("_View", bound="View")
_Obj = TypeVar("_Obj", bound=object)


def _wrap_py_object(obj: _T | py_object[_T]) -> py_object[_T]:
Expand Down Expand Up @@ -85,11 +97,15 @@ def __init__(self, obj: _T, ref: bool = REF_DEFAULT) -> None:
_ = self.mem_allocated # cache allocated property

def __repr__(self) -> str:
"""Return a string representation of the view."""
addr = self._pyobject.address
py_obj_cls = self._pyobject.__class__.__name__
# If we have an instance dict (subclass), include base type in repr
has_dict = self._pyobject.ob_type.contents.tp_dictoffset != 0
base = f"[{self._base_type.__name__}]" if has_dict else ""
# Or if we are the `View` class
base = ""
if has_dict or self.__class__ is View:
base = f"[{self._base_type.__name__}]"
return f"{self.__class__.__name__}{base}(<{py_obj_cls} at {addr:#04x}>)"

def info(self, types: bool = True, arr_max: int | None = 64) -> str:
Expand Down Expand Up @@ -263,56 +279,79 @@ def move_to(self, dst: View, start: int = 8) -> None:
"""
if not isinstance(dst, View):
raise TypeError(f"Expected View, got {type(dst).__name__!r}")
# Materialize instance dicts in case we need to copy
with suppress(AttributeError):
self._pyobject.GetAttr("__dict__")
with suppress(AttributeError):
dst._pyobject.GetAttr("__dict__")
# If we have an instance dict, copy it first
dict_ptr = self._pyobject.instance_dict()
if dict_ptr is not None:
dict_addr = ctypes.addressof(dict_ptr)
dict_offset = dict_addr - self._pyobject.address
ctypes.memmove(
dst._pyobject.address + dict_offset,
ctypes.c_void_p(dict_addr),
PTR_SIZE,
)
ctypes.memmove(
dst._pyobject.address + start,
self._pyobject.address + start,
self.mem_size - start,
)

@unsafe
def move_from(self, other: _V) -> _V:
@overload
def move_from(self, other: _View) -> _View:
...

@overload
def move_from(self, other: _Obj) -> View[_Obj]:
...

def move_from(self, other):
"""Moves data at other View to this View."""
from einspect.views import factory

# Store our repr
self_repr = repr(self)
# Store our current address
addr = self._pyobject.address
if not isinstance(other, View):
with ExitStack() as stack:
# Add a temp ref to prevent GC before we're done moving
Py.IncRef(other)
stack.callback(Py.DecRef, other)
# Take a deepcopy to prevent issues with members being GC'd
other = deepcopy(other)
# Prevent new deepcopy being dropped by adding a reference
Py.IncRef(other)
other = factory.view(other)
other = factory.view(other) # type: ignore

# Check move safety if not in unsafe context
if not self._unsafe:
_check_move(self, other)

# Store our address
addr = self._pyobject.address
# Move other to our pyobject address
with other.unsafe():
other.move_to(self)
# Increment other refcount
other._pyobject.IncRef()
# Return a new view of ourselves
obj = PyObj_FromPtr(addr)
# Increment ref count before returning
Py.IncRef(obj)
v = factory.view(obj)
log.debug(f"Moved {other} to {self_repr} -> {v}")
log.debug(f"New ref count: {v.ref_count}")
# Drop the current view
# Drop old view
self.drop()
return v

def __lshift__(self, other: _V) -> _V:
@overload
def __lshift__(self, other: _View) -> _View:
...

@overload
def __lshift__(self, other: _Obj) -> View[_Obj]:
...

def __lshift__(self, other):
"""Moves data at other View to this View."""
return self.move_from(other)

def __invert__(self) -> _T:
"""Returns the base of this view as object."""
# Prioritize strong ref if it exists
if self._base is not None:
return self._base.value
return self.base.value
return self._base
return self.base


class VarView(View[_T, _KT, _VT]):
Expand Down
Loading

0 comments on commit cd2422d

Please sign in to comment.