purepy-lang 0.1.0__tar.gz
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.
- purepy_lang-0.1.0/PKG-INFO +8 -0
- purepy_lang-0.1.0/README.md +36 -0
- purepy_lang-0.1.0/purepy/__init__.py +5 -0
- purepy_lang-0.1.0/purepy/py.typed +0 -0
- purepy_lang-0.1.0/purepy/value.py +38 -0
- purepy_lang-0.1.0/purepy_lang.egg-info/PKG-INFO +8 -0
- purepy_lang-0.1.0/purepy_lang.egg-info/SOURCES.txt +10 -0
- purepy_lang-0.1.0/purepy_lang.egg-info/dependency_links.txt +1 -0
- purepy_lang-0.1.0/purepy_lang.egg-info/top_level.txt +1 -0
- purepy_lang-0.1.0/pyproject.toml +21 -0
- purepy_lang-0.1.0/setup.cfg +4 -0
- purepy_lang-0.1.0/tests/test_value.py +164 -0
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: purepy-lang
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: The small immutable-record runtime for verified PurePy programs
|
|
5
|
+
Project-URL: Repository, https://github.com/dwrtz/purepy
|
|
6
|
+
Project-URL: Documentation, https://github.com/dwrtz/purepy/blob/main/docs/LANGUAGE_GUIDE.md
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Requires-Python: >=3.12
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# PurePy runtime
|
|
2
|
+
|
|
3
|
+
The package supplies the single runtime decorator admitted by PurePy:
|
|
4
|
+
|
|
5
|
+
```python
|
|
6
|
+
from purepy import value
|
|
7
|
+
|
|
8
|
+
@value
|
|
9
|
+
class User:
|
|
10
|
+
user_id: int
|
|
11
|
+
name: str
|
|
12
|
+
|
|
13
|
+
user = User(1, name="Ada")
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Records are frozen, slotted, nominal, and data-only. The static verifier validates
|
|
17
|
+
field types and deep immutability, including homogeneous tuples, optional values,
|
|
18
|
+
project records, and manifest-declared external values. The runtime permits Python
|
|
19
|
+
3.14 deferred annotations without resolving forward records at decoration time.
|
|
20
|
+
There are no defaults, user methods, or inheritance. The host must supply values
|
|
21
|
+
of the exact declared types; the decorator does not repeat static type checking.
|
|
22
|
+
|
|
23
|
+
Use uv to create the repository's Python 3.14 virtual environment and install the
|
|
24
|
+
runtime. From the repository root, run:
|
|
25
|
+
|
|
26
|
+
```sh
|
|
27
|
+
make setup
|
|
28
|
+
make python-test
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Python 3.12 and later execute this support package; PurePy source syntax targets
|
|
32
|
+
3.14. The managed development interpreter is `.venv/bin/python`. This runtime
|
|
33
|
+
performs no I/O and has no registry, verifier integration,
|
|
34
|
+
effect interpreter, or application framework. The verifier is authoritative about
|
|
35
|
+
class shapes, recursive records, and field types; arbitrary unverified callers
|
|
36
|
+
remain responsible for the contracts they pass into verified code.
|
|
File without changes
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""Data-only nominal records, with no registry or runtime effect machinery.
|
|
2
|
+
|
|
3
|
+
The static verifier remains authoritative about source syntax. These checks catch
|
|
4
|
+
accidental misuse by ordinary Python callers; they are not a Python sandbox.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from dataclasses import dataclass, fields
|
|
8
|
+
_CLASS_METADATA = frozenset({
|
|
9
|
+
"__module__", "__qualname__", "__doc__", "__annotations__",
|
|
10
|
+
"__dict__", "__weakref__", "__firstlineno__", "__static_attributes__",
|
|
11
|
+
"__annotate__", "__annotate_func__", "__annotations_cache__",
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _reject_subclass(cls, **kwargs):
|
|
16
|
+
raise TypeError("@value records cannot be subclassed")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def value(cls):
|
|
20
|
+
"""Create a frozen, slotted record from annotated fields without defaults.
|
|
21
|
+
|
|
22
|
+
Positional and explicit keyword construction use declaration order. The
|
|
23
|
+
verifier validates field types and rejects recursive record definitions. The
|
|
24
|
+
host must supply exact Pure Values, including immutable external values whose
|
|
25
|
+
contracts live in manifests. This runtime neither loads manifests nor eagerly
|
|
26
|
+
resolves Python 3.14 deferred annotations.
|
|
27
|
+
"""
|
|
28
|
+
if type(cls) is not type or cls.__bases__ != (object,):
|
|
29
|
+
raise TypeError("@value requires a plain class without inheritance or a metaclass")
|
|
30
|
+
for name in cls.__dict__:
|
|
31
|
+
if name not in _CLASS_METADATA:
|
|
32
|
+
raise TypeError(f"@value class bodies contain only annotated fields, not {name!r}")
|
|
33
|
+
record = dataclass(frozen=True, slots=True)(cls)
|
|
34
|
+
for field in fields(record):
|
|
35
|
+
if field.name.startswith("__") and field.name.endswith("__"):
|
|
36
|
+
raise TypeError("@value fields cannot use reserved double-underscore names")
|
|
37
|
+
record.__init_subclass__ = classmethod(_reject_subclass)
|
|
38
|
+
return record
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: purepy-lang
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: The small immutable-record runtime for verified PurePy programs
|
|
5
|
+
Project-URL: Repository, https://github.com/dwrtz/purepy
|
|
6
|
+
Project-URL: Documentation, https://github.com/dwrtz/purepy/blob/main/docs/LANGUAGE_GUIDE.md
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Requires-Python: >=3.12
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
purepy
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools==80.9.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "purepy-lang"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "The small immutable-record runtime for verified PurePy programs"
|
|
9
|
+
requires-python = ">=3.12"
|
|
10
|
+
classifiers = ["Programming Language :: Python :: 3"]
|
|
11
|
+
|
|
12
|
+
[project.urls]
|
|
13
|
+
Repository = "https://github.com/dwrtz/purepy"
|
|
14
|
+
Documentation = "https://github.com/dwrtz/purepy/blob/main/docs/LANGUAGE_GUIDE.md"
|
|
15
|
+
|
|
16
|
+
[tool.setuptools.packages.find]
|
|
17
|
+
where = ["."]
|
|
18
|
+
include = ["purepy"]
|
|
19
|
+
|
|
20
|
+
[tool.setuptools.package-data]
|
|
21
|
+
purepy = ["py.typed"]
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"""Runtime parity and defensive checks for the sole PurePy decorator."""
|
|
2
|
+
|
|
3
|
+
import dataclasses
|
|
4
|
+
import sys
|
|
5
|
+
import unittest
|
|
6
|
+
|
|
7
|
+
from purepy import value
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@value
|
|
11
|
+
class User:
|
|
12
|
+
identifier: int
|
|
13
|
+
name: str
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@value
|
|
17
|
+
class Group:
|
|
18
|
+
users: tuple[User, ...]
|
|
19
|
+
title: str | None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ValueTests(unittest.TestCase):
|
|
23
|
+
def test_positional_keyword_fields_and_equality(self):
|
|
24
|
+
first = User(1, "Ada")
|
|
25
|
+
self.assertEqual(first, User(name="Ada", identifier=1))
|
|
26
|
+
self.assertEqual(first.identifier, 1)
|
|
27
|
+
self.assertEqual(first.name, "Ada")
|
|
28
|
+
self.assertNotEqual(first, User(2, "Ada"))
|
|
29
|
+
self.assertEqual(tuple(field.name for field in dataclasses.fields(User)), ("identifier", "name"))
|
|
30
|
+
|
|
31
|
+
def test_nominal_equality(self):
|
|
32
|
+
@value
|
|
33
|
+
class Other:
|
|
34
|
+
identifier: int
|
|
35
|
+
name: str
|
|
36
|
+
self.assertNotEqual(User(1, "Ada"), Other(1, "Ada"))
|
|
37
|
+
|
|
38
|
+
def test_frozen_slots(self):
|
|
39
|
+
user = User(1, "Ada")
|
|
40
|
+
self.assertFalse(hasattr(user, "__dict__"))
|
|
41
|
+
with self.assertRaises(dataclasses.FrozenInstanceError):
|
|
42
|
+
user.name = "Grace"
|
|
43
|
+
with self.assertRaises(dataclasses.FrozenInstanceError):
|
|
44
|
+
del user.name
|
|
45
|
+
with self.assertRaises((AttributeError, TypeError)):
|
|
46
|
+
user.other = 2
|
|
47
|
+
|
|
48
|
+
def test_deep_values(self):
|
|
49
|
+
group = Group((User(1, "Ada"),), None)
|
|
50
|
+
self.assertEqual(group, Group(users=(User(1, "Ada"),), title=None))
|
|
51
|
+
with self.assertRaises(dataclasses.FrozenInstanceError):
|
|
52
|
+
group.users[0].name = "Grace"
|
|
53
|
+
with self.assertRaises(TypeError):
|
|
54
|
+
group.users[0] = User(2, "Grace")
|
|
55
|
+
|
|
56
|
+
def test_wrong_constructor_binding(self):
|
|
57
|
+
for call in (lambda: User(1), lambda: User(1, "Ada", "extra"),
|
|
58
|
+
lambda: User(1, name="Ada", identifier=2),
|
|
59
|
+
lambda: User(1, label="Ada")):
|
|
60
|
+
with self.assertRaises(TypeError):
|
|
61
|
+
call()
|
|
62
|
+
|
|
63
|
+
def test_no_inheritance(self):
|
|
64
|
+
with self.assertRaises(TypeError):
|
|
65
|
+
class Child(User):
|
|
66
|
+
pass
|
|
67
|
+
class Base:
|
|
68
|
+
pass
|
|
69
|
+
with self.assertRaises(TypeError):
|
|
70
|
+
@value
|
|
71
|
+
class Child(Base):
|
|
72
|
+
identifier: int
|
|
73
|
+
|
|
74
|
+
def test_reject_methods_defaults_constants_properties_nested_classes(self):
|
|
75
|
+
for extra in ({"x": 1}, {"f": lambda self: 1}, {"CONSTANT": 1},
|
|
76
|
+
{"field": property(lambda self: 1)}, {"Nested": type("Nested", (), {})},
|
|
77
|
+
{"__init__": lambda self: None}, {"__slots__": ()}):
|
|
78
|
+
with self.subTest(extra=extra), self.assertRaises(TypeError):
|
|
79
|
+
value(type("Invalid", (), {"__annotations__": {"x": int}, **extra}))
|
|
80
|
+
|
|
81
|
+
def test_external_value_contract_is_owned_by_manifest_and_host(self):
|
|
82
|
+
@dataclasses.dataclass(frozen=True, slots=True)
|
|
83
|
+
class ExternalValue:
|
|
84
|
+
text: str
|
|
85
|
+
|
|
86
|
+
@value
|
|
87
|
+
class ContainsExternal:
|
|
88
|
+
external: ExternalValue
|
|
89
|
+
|
|
90
|
+
self.assertEqual(ContainsExternal(ExternalValue("opaque")),
|
|
91
|
+
ContainsExternal(ExternalValue("opaque")))
|
|
92
|
+
|
|
93
|
+
def test_opaque_value_construction_and_field_reads_do_not_dispatch(self):
|
|
94
|
+
@dataclasses.dataclass(frozen=True, slots=True)
|
|
95
|
+
class Opaque:
|
|
96
|
+
number: int
|
|
97
|
+
|
|
98
|
+
def __eq__(self, other):
|
|
99
|
+
raise AssertionError("opaque equality must not be invoked")
|
|
100
|
+
|
|
101
|
+
def __bool__(self):
|
|
102
|
+
raise AssertionError("opaque truthiness must not be invoked")
|
|
103
|
+
|
|
104
|
+
def __repr__(self):
|
|
105
|
+
raise AssertionError("opaque formatting must not be invoked")
|
|
106
|
+
|
|
107
|
+
def __hash__(self):
|
|
108
|
+
raise AssertionError("opaque hashing must not be invoked")
|
|
109
|
+
|
|
110
|
+
@value
|
|
111
|
+
class Envelope:
|
|
112
|
+
tokens: tuple[Opaque | None, ...]
|
|
113
|
+
|
|
114
|
+
token = Opaque(7)
|
|
115
|
+
envelope = Envelope(tokens=(token, None))
|
|
116
|
+
self.assertIs(envelope.tokens[0], token)
|
|
117
|
+
self.assertIsNone(envelope.tokens[1])
|
|
118
|
+
with self.assertRaises(dataclasses.FrozenInstanceError):
|
|
119
|
+
envelope.tokens = ()
|
|
120
|
+
|
|
121
|
+
@unittest.skipIf(sys.version_info < (3, 14), "deferred annotation syntax requires Python 3.14")
|
|
122
|
+
def test_forward_record_annotations_on_python314(self):
|
|
123
|
+
# No source-level quoted annotations or future import is needed on 3.14.
|
|
124
|
+
namespace = {"value": value, "__name__": __name__}
|
|
125
|
+
exec("""
|
|
126
|
+
@value
|
|
127
|
+
class Outer:
|
|
128
|
+
item: Later
|
|
129
|
+
|
|
130
|
+
@value
|
|
131
|
+
class Later:
|
|
132
|
+
number: int
|
|
133
|
+
""", namespace)
|
|
134
|
+
outer = namespace["Outer"](namespace["Later"](7))
|
|
135
|
+
self.assertEqual(outer.item.number, 7)
|
|
136
|
+
|
|
137
|
+
def test_primitive_optional_nested_tuple_fields(self):
|
|
138
|
+
@value
|
|
139
|
+
class Primitives:
|
|
140
|
+
flag: bool
|
|
141
|
+
number: float
|
|
142
|
+
data: bytes
|
|
143
|
+
nothing: None
|
|
144
|
+
tuples: tuple[tuple[int, ...], ...]
|
|
145
|
+
# Python evaluates an annotation spelled None as None, not NoneType.
|
|
146
|
+
result = Primitives(True, 1.5, b"x", None, ((1, 2), ()))
|
|
147
|
+
self.assertEqual(result.tuples, ((1, 2), ()))
|
|
148
|
+
|
|
149
|
+
def test_no_custom_metaclass(self):
|
|
150
|
+
class Meta(type):
|
|
151
|
+
pass
|
|
152
|
+
with self.assertRaises(TypeError):
|
|
153
|
+
value(Meta("Invalid", (), {"__annotations__": {"x": int}}))
|
|
154
|
+
|
|
155
|
+
def test_empty_record(self):
|
|
156
|
+
@value
|
|
157
|
+
class Empty:
|
|
158
|
+
"""An empty record is data-only."""
|
|
159
|
+
self.assertEqual(Empty(), Empty())
|
|
160
|
+
self.assertFalse(hasattr(Empty(), "__dict__"))
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
if __name__ == "__main__":
|
|
164
|
+
unittest.main()
|