Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ownership semantics #542

Draft
wants to merge 8 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# Changelog

## Unreleased

- Add concept of ownership: only containers owned by calling
code may be mutated (#542)

## Version 0.8.0 (November 5, 2022)

Release highlights:
Expand Down
9 changes: 8 additions & 1 deletion pyanalyze/attributes.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import qcore

from .annotations import Context, type_from_annotations, type_from_runtime
from .extensions import Mutable
from .options import Options, PyObjectSequenceOption
from .safe import safe_isinstance, safe_issubclass
from .signature import MaybeSignature
Expand All @@ -30,6 +31,7 @@
KnownValue,
KnownValueWithTypeVars,
MultiValuedValue,
make_mutable,
set_self,
SubclassValue,
TypedValue,
Expand Down Expand Up @@ -89,9 +91,11 @@ def get_generic_bases(

def get_attribute(ctx: AttrContext) -> Value:
root_value = ctx.root_value
should_own = False
if isinstance(root_value, TypeVarValue):
root_value = root_value.get_fallback_value()
elif isinstance(root_value, AnnotatedValue):
should_own = any(root_value.get_custom_check_of_type(Mutable))
root_value = root_value.value
if isinstance(root_value, KnownValue):
attribute_value = _get_attribute_from_known(root_value.val, ctx)
Expand Down Expand Up @@ -137,7 +141,10 @@ def get_attribute(ctx: AttrContext) -> Value:
) and isinstance(ctx.root_value, AnnotatedValue):
for guard in ctx.root_value.get_metadata_of_type(HasAttrExtension):
if guard.attribute_name == KnownValue(ctx.attr):
return guard.attribute_type
attribute_value = guard.attribute_type
break
if should_own and attribute_value is not UNINITIALIZED_VALUE:
attribute_value = make_mutable(attribute_value)
return attribute_value


Expand Down
2 changes: 2 additions & 0 deletions pyanalyze/error_code.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ class ErrorCode(enum.Enum):
invalid_annotated_assignment = 79
unused_assignment = 80
incompatible_yield = 81
disallowed_mutation = 82


# Allow testing unannotated functions without too much fuss
Expand Down Expand Up @@ -219,6 +220,7 @@ class ErrorCode(enum.Enum):
ErrorCode.invalid_annotated_assignment: "Invalid annotated assignment",
ErrorCode.unused_assignment: "Assigned value is never used",
ErrorCode.incompatible_yield: "Incompatible yield type",
ErrorCode.disallowed_mutation: "Mutation of object that does not allow mutation",
}


Expand Down
34 changes: 30 additions & 4 deletions pyanalyze/extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
)

import typing_extensions
from typing_extensions import Literal, NoReturn
from typing_extensions import Annotated, Literal, NoReturn

import pyanalyze

Expand All @@ -39,6 +39,8 @@
if TYPE_CHECKING:
from .value import AnySource, CanAssign, CanAssignContext, TypeVarMap, Value

_T = TypeVar("_T")


class CustomCheck:
"""A mechanism for extending the type system with user-defined checks.
Expand Down Expand Up @@ -146,6 +148,33 @@ def _is_disallowed(self, value: "Value") -> bool:
)


@dataclass(frozen=True)
class Mutable(CustomCheck):
"""Custom check that indicates that a mutable value is mutated. For example,
a function that mutates a list should accept an argument of type
``Annotated[List[T], Mutable()]``.
"""

def can_assign(self, value: "Value", ctx: "CanAssignContext") -> "CanAssign":
for val in pyanalyze.value.flatten_values(value, unwrap_annotated=False):
if isinstance(val, pyanalyze.value.AnnotatedValue):
if any(val.get_custom_check_of_type(Mutable)):
continue
val = val.value
if isinstance(val, pyanalyze.value.AnyValue):
continue
return pyanalyze.value.CanAssignError(
f"Value {val} is not owned and may not be mutated",
error_code=pyanalyze.error_code.ErrorCode.disallowed_mutation,
)
return {}


def make_mutable(obj: _T) -> Annotated[_T, Mutable()]:
"""Unsafely mark an object as mutable."""
return obj


class _AsynqCallableMeta(type):
def __getitem__(
self, params: Tuple[Union[Literal[Ellipsis], List[object]], object]
Expand Down Expand Up @@ -372,9 +401,6 @@ def __call__(self) -> NoReturn:
raise NotImplementedError("just here to fool typing._type_check")


_T = TypeVar("_T")


def reveal_type(value: _T) -> _T:
"""Inspect the inferred type of an expression.

Expand Down
2 changes: 2 additions & 0 deletions pyanalyze/functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
SubclassValue,
TypedValue,
TypeVarValue,
make_mutable,
unite_values,
UnpackedValue,
Value,
Expand Down Expand Up @@ -341,6 +342,7 @@ def compute_parameters(
else:
# normal method
value = enclosing_class
value = make_mutable(value)
else:
# This is meant to exclude methods in nested classes. It's a bit too
# conservative for cases such as a function nested in a method nested in a
Expand Down
Loading