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.
@@ -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.
@@ -0,0 +1,5 @@
1
+ """Runtime support for the PurePy language. Verification is performed separately."""
2
+
3
+ from .value import value
4
+
5
+ __all__ = ["value"]
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,10 @@
1
+ README.md
2
+ pyproject.toml
3
+ purepy/__init__.py
4
+ purepy/py.typed
5
+ purepy/value.py
6
+ purepy_lang.egg-info/PKG-INFO
7
+ purepy_lang.egg-info/SOURCES.txt
8
+ purepy_lang.egg-info/dependency_links.txt
9
+ purepy_lang.egg-info/top_level.txt
10
+ tests/test_value.py
@@ -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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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()