pytypehintstore 0.0.4__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.
- pytypehintstore-0.0.4/.github/dependabot.yml +6 -0
- pytypehintstore-0.0.4/.github/workflows/ci.yml +33 -0
- pytypehintstore-0.0.4/.github/workflows/publish.yml +24 -0
- pytypehintstore-0.0.4/.gitignore +19 -0
- pytypehintstore-0.0.4/CHANGELOG.md +93 -0
- pytypehintstore-0.0.4/CIERRE.md +275 -0
- pytypehintstore-0.0.4/INFORME.md +293 -0
- pytypehintstore-0.0.4/LICENSE +21 -0
- pytypehintstore-0.0.4/PKG-INFO +369 -0
- pytypehintstore-0.0.4/README.md +341 -0
- pytypehintstore-0.0.4/example.py +55 -0
- pytypehintstore-0.0.4/pyproject.toml +51 -0
- pytypehintstore-0.0.4/src/pytypehintstore/__init__.py +11 -0
- pytypehintstore-0.0.4/src/pytypehintstore/codec.py +249 -0
- pytypehintstore-0.0.4/src/pytypehintstore/errors.py +19 -0
- pytypehintstore-0.0.4/src/pytypehintstore/fingerprint.py +80 -0
- pytypehintstore-0.0.4/src/pytypehintstore/lockfile.py +199 -0
- pytypehintstore-0.0.4/src/pytypehintstore/py.typed +0 -0
- pytypehintstore-0.0.4/src/pytypehintstore/store.py +421 -0
- pytypehintstore-0.0.4/tests/conftest.py +270 -0
- pytypehintstore-0.0.4/tests/shared.py +34 -0
- pytypehintstore-0.0.4/tests/test_boundary.py +67 -0
- pytypehintstore-0.0.4/tests/test_codec.py +896 -0
- pytypehintstore-0.0.4/tests/test_identity.py +226 -0
- pytypehintstore-0.0.4/tests/test_lockfile.py +476 -0
- pytypehintstore-0.0.4/tests/test_persistence.py +648 -0
- pytypehintstore-0.0.4/tests/test_store.py +482 -0
- pytypehintstore-0.0.4/tests/test_stress_depth.py +801 -0
- pytypehintstore-0.0.4/tests/test_stress_generated.py +1452 -0
- pytypehintstore-0.0.4/tests/test_stress_hostile_file.py +759 -0
- pytypehintstore-0.0.4/tests/test_stress_unions.py +916 -0
- pytypehintstore-0.0.4/tests/test_stress_values.py +939 -0
- pytypehintstore-0.0.4/tests/test_writer.py +615 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [master]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
concurrency:
|
|
9
|
+
group: ${{ github.workflow }}-${{ github.ref }}
|
|
10
|
+
cancel-in-progress: true
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
check:
|
|
14
|
+
# Both, because the lock is the one part written twice: an open handle on
|
|
15
|
+
# Windows, an advisory flock on POSIX. A single runner would leave half of
|
|
16
|
+
# it untested.
|
|
17
|
+
runs-on: ${{ matrix.os }}
|
|
18
|
+
strategy:
|
|
19
|
+
fail-fast: false
|
|
20
|
+
matrix:
|
|
21
|
+
os: [ubuntu-latest, windows-latest]
|
|
22
|
+
python-version: ["3.11", "3.12", "3.13"]
|
|
23
|
+
steps:
|
|
24
|
+
- uses: actions/checkout@v7
|
|
25
|
+
- uses: actions/setup-python@v7
|
|
26
|
+
with:
|
|
27
|
+
python-version: ${{ matrix.python-version }}
|
|
28
|
+
- uses: astral-sh/setup-uv@v7
|
|
29
|
+
with:
|
|
30
|
+
enable-cache: true
|
|
31
|
+
- run: uv pip install --system -e ".[dev]"
|
|
32
|
+
- run: mypy src
|
|
33
|
+
- run: pytest
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
name: Publish
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
publish:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
permissions:
|
|
11
|
+
id-token: write
|
|
12
|
+
steps:
|
|
13
|
+
- uses: actions/checkout@v7
|
|
14
|
+
- uses: actions/setup-python@v7
|
|
15
|
+
with:
|
|
16
|
+
python-version: "3.11"
|
|
17
|
+
- uses: astral-sh/setup-uv@v7
|
|
18
|
+
with:
|
|
19
|
+
enable-cache: true
|
|
20
|
+
- run: uv pip install --system -e ".[dev]"
|
|
21
|
+
- run: mypy src
|
|
22
|
+
- run: pytest
|
|
23
|
+
- run: uv build
|
|
24
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
__pycache__/
|
|
2
|
+
*.py[cod]
|
|
3
|
+
build/
|
|
4
|
+
dist/
|
|
5
|
+
*.egg-info/
|
|
6
|
+
.venv/
|
|
7
|
+
venv/
|
|
8
|
+
.pytest_cache/
|
|
9
|
+
.mypy_cache/
|
|
10
|
+
.vscode/
|
|
11
|
+
.idea/
|
|
12
|
+
|
|
13
|
+
# What the example leaves behind: its directory, the store and the copies it
|
|
14
|
+
# rotates, plus a part file from a dump that never landed.
|
|
15
|
+
data/
|
|
16
|
+
*.json.lock
|
|
17
|
+
*.json.part
|
|
18
|
+
tasks.json
|
|
19
|
+
tasks.*.json
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [0.0.4]
|
|
4
|
+
|
|
5
|
+
One library on both platforms: the lock now holds the same contract on POSIX
|
|
6
|
+
that it held on Windows, and the checker agrees on both.
|
|
7
|
+
|
|
8
|
+
- The lock is settled by the operating system on POSIX too. It was an open
|
|
9
|
+
handle on Windows — a file Windows will not unlink while it is open — and a
|
|
10
|
+
bare `O_EXCL` everywhere else, which POSIX does not defend: two processes
|
|
11
|
+
reaching for one orphaned lockfile could both unlink it and both create their
|
|
12
|
+
own, ending as two live owners of one file. POSIX now takes an advisory
|
|
13
|
+
`flock`, which the kernel drops when a process dies, and checks the inode
|
|
14
|
+
afterwards in case the file was replaced between the open and the lock.
|
|
15
|
+
- An orphan needs no stealing on POSIX: what makes it an orphan is that its
|
|
16
|
+
`flock` died with its owner. The behaviour a caller sees is unchanged — one
|
|
17
|
+
owner, orphans reclaimed, the same message naming the live PID.
|
|
18
|
+
- `release` unlinks before closing on POSIX and after closing on Windows, each
|
|
19
|
+
being the order that cannot hand a second owner the lock.
|
|
20
|
+
- `mypy` passes on both platforms. The Windows probe is declared under
|
|
21
|
+
`sys.platform` rather than `os.name`, which is the form a checker reads as a
|
|
22
|
+
platform guard, so `ctypes.WinDLL` is not looked for on Linux.
|
|
23
|
+
- CI runs the suite on `ubuntu-latest` and `windows-latest`, over 3.11, 3.12
|
|
24
|
+
and 3.13. Half of the lock only exists on one of them.
|
|
25
|
+
- New test: two processes reaching for one orphan at the same instant leave a
|
|
26
|
+
single owner. It is the property both halves exist to hold, and it runs on
|
|
27
|
+
both.
|
|
28
|
+
- One difference is left, and declared: with a lockfile nobody is holding, POSIX
|
|
29
|
+
trusts the `flock` and takes it over — so a recycled pid cannot lock a path
|
|
30
|
+
out there — while Windows trusts the pid written in it and refuses. Two live
|
|
31
|
+
stores behave identically on both.
|
|
32
|
+
|
|
33
|
+
## [0.0.2]
|
|
34
|
+
|
|
35
|
+
Out of a stress campaign: 1150 generated schemas, four adversarial fronts, and
|
|
36
|
+
the suite from 200 tests to 1583.
|
|
37
|
+
|
|
38
|
+
- Fixed, data loss: `fingerprint` walked a schema as a tree when it is a graph —
|
|
39
|
+
the core compiles a class once and shares it between every field naming it —
|
|
40
|
+
so a class the core compiled in a millisecond could take minutes to
|
|
41
|
+
fingerprint, and `store_of` hung with no error and no output. Each dataclass
|
|
42
|
+
is now written once and referred back to. Schemas with the same class in two
|
|
43
|
+
fields change their fingerprint, and so their file name.
|
|
44
|
+
- Fixed, data loss: a lone surrogate was accepted by `add`, could never be
|
|
45
|
+
written to a UTF-8 file, and froze every later dump — the failure surfacing
|
|
46
|
+
only at `close()`, as an error from another library. Such a row is refused at
|
|
47
|
+
`add` with `SchemaValueError: cannot be written as UTF-8`, after the core has
|
|
48
|
+
had its say.
|
|
49
|
+
- Fixed, data loss: a union whose options share a transport name — an enum class
|
|
50
|
+
called `date` beside `date` itself — routed values to the wrong branch, and a
|
|
51
|
+
stored member came back as a plain string. The store now refuses the schema at
|
|
52
|
+
open, naming the field and the shared name, before the lockfile is taken. Real
|
|
53
|
+
collisions only: the same enum with nothing to collide with still works.
|
|
54
|
+
- A `$type`/`$value` wrapper is those two keys and nothing else. A key beside
|
|
55
|
+
them, or a `$value` that is itself a wrapper, travels intact for the core to
|
|
56
|
+
refuse instead of being read anyway and erased by the next dump.
|
|
57
|
+
- The format version is checked by type as well as by value, so a `1.0` or a
|
|
58
|
+
`true` in `v` is no longer read as version 1.
|
|
59
|
+
- Packaging: classifiers, keywords and project URLs for PyPI.
|
|
60
|
+
|
|
61
|
+
## [0.0.1]
|
|
62
|
+
|
|
63
|
+
First release. Rows of one `pytypehint`-validated dataclass, kept in memory and
|
|
64
|
+
mirrored to a JSON file a person can open and edit.
|
|
65
|
+
|
|
66
|
+
- `store_of(cls, directory, *, debounce=2.0, keep=5)` opens the store of one
|
|
67
|
+
dataclass on a file named for the class and the fingerprint of its schema.
|
|
68
|
+
Reads answer from memory; writes mutate the dict at once and a writer thread
|
|
69
|
+
dumps the whole state `debounce` seconds after the burst of writes stops.
|
|
70
|
+
- `add` and `put` accept a row by making the round trip it will make anyway:
|
|
71
|
+
the instance is encoded to its transport form and rebuilt through the schema,
|
|
72
|
+
so what the store holds always came out of the constructor and always
|
|
73
|
+
survives the file. Validation failures are the core's own `SchemaTypeError`
|
|
74
|
+
and `SchemaValueError`, untouched; the store adds no second validation
|
|
75
|
+
system.
|
|
76
|
+
- The file carries the transport form — ISO text for a date or a time, the
|
|
77
|
+
member name for an enum, an object for a nested dataclass — so it stays
|
|
78
|
+
readable and editable. A row that cannot be read back names the file, the row
|
|
79
|
+
and the core's error.
|
|
80
|
+
- Dumps are atomic: `.part`, `fsync`, `os.replace`. A dump that fails leaves
|
|
81
|
+
nothing of itself behind, keeps the state unwritten and is retried; the
|
|
82
|
+
writer thread never dies of it. The previous file is rotated aside with a
|
|
83
|
+
nanosecond stamp and copies beyond `keep` are pruned.
|
|
84
|
+
- `close()` dumps what is pending, stops the writer and releases the lock. It is
|
|
85
|
+
idempotent, and a failure on the way down reaches every caller that was
|
|
86
|
+
closing the same store. An `atexit` hook closes a store the caller forgot, on
|
|
87
|
+
a normal interpreter exit.
|
|
88
|
+
- One process owns a store. The lockfile holds the owner's PID and stays open
|
|
89
|
+
for as long as the lock is held, so a second process is refused at startup
|
|
90
|
+
rather than corrupting the file later, and two processes racing for the same
|
|
91
|
+
orphaned lock cannot both end up owning it.
|
|
92
|
+
- Three exceptions, all subclasses of `StoreError`: `StoreLockedError`,
|
|
93
|
+
`StoreLoadError`, and `StoreError` itself for an operation on a closed store.
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
# Cierre de 0.0.2
|
|
2
|
+
|
|
3
|
+
Temporal, para revisión humana. No forma parte del repo publicable.
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
$ python -m pytest -q
|
|
7
|
+
1583 passed, 13 xfailed in 29.44s
|
|
8
|
+
|
|
9
|
+
$ python -m mypy # src
|
|
10
|
+
Success: no issues found in 6 source files
|
|
11
|
+
$ python -m mypy tests
|
|
12
|
+
Success: no issues found in 14 source files
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
| | 0.0.1 | 0.0.2 |
|
|
16
|
+
|---|---:|---:|
|
|
17
|
+
| tests | 200 | **1583** (+13 xfail) |
|
|
18
|
+
| tiempo de suite en frío | 6,9 s | **29,4 s** (presupuesto 60 s) |
|
|
19
|
+
| líneas de `src/` | 861 | **928** |
|
|
20
|
+
| módulos | 5 | 6 |
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Fase 1 — el arreglo del enum, completo
|
|
25
|
+
|
|
26
|
+
### `src/pytypehintstore/store.py`
|
|
27
|
+
|
|
28
|
+
Importa `List` y `Struct`, ya públicos y ya usados por el codec:
|
|
29
|
+
|
|
30
|
+
```diff
|
|
31
|
+
-from pytypehint import SchemaTypeError, SchemaValueError, struct_of
|
|
32
|
+
+from pytypehint import List, SchemaTypeError, SchemaValueError, Struct, struct_of
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
La guarda corre en `__init__`, después de derivar el nombre del archivo (para
|
|
36
|
+
poder citarlo) y **antes** de `folder.mkdir` y de `acquire`:
|
|
37
|
+
|
|
38
|
+
```diff
|
|
39
|
+
self._path = folder / name
|
|
40
|
+
self._name = str(Path(directory) / name)
|
|
41
|
+
+ self._ambiguous()
|
|
42
|
+
self._debounce = float(debounce)
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
El método, junto a `_open`:
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
# Two options of one union that answer to the same transport name are
|
|
49
|
+
# indistinguishable in the file: the wrapper names an option and the reader
|
|
50
|
+
# takes the first that answers. The core allows the pair — an enum class
|
|
51
|
+
# called `date` competes with `date` itself, in different namespaces to it —
|
|
52
|
+
# so the store refuses at the door rather than routing a value to the wrong
|
|
53
|
+
# branch and handing back something nobody stored.
|
|
54
|
+
def _ambiguous(self) -> None:
|
|
55
|
+
found = _shared_name((self._schema,), (), set())
|
|
56
|
+
|
|
57
|
+
if found is None:
|
|
58
|
+
return
|
|
59
|
+
|
|
60
|
+
path, shared = found
|
|
61
|
+
where = ": ".join(path)
|
|
62
|
+
raise StoreError(
|
|
63
|
+
f"{self._name}: {where}: two options of a union share the transport "
|
|
64
|
+
f"name {shared!r}: rename one of the classes")
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Y el recorrido, a nivel de módulo (18 líneas):
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
# The first pair of options sharing a transport name, as (path, name), or None.
|
|
71
|
+
# `seen` keeps a recursive schema from being walked forever.
|
|
72
|
+
def _shared_name(shapes, path, seen):
|
|
73
|
+
names = set()
|
|
74
|
+
|
|
75
|
+
for shape in shapes:
|
|
76
|
+
# A Struct carries its $type inside the object and never competes for
|
|
77
|
+
# the wrapper's, so a dataclass and an enum may share one name — the
|
|
78
|
+
# core allows exactly that, and the codec keeps them apart.
|
|
79
|
+
if type(shape) is Struct:
|
|
80
|
+
continue
|
|
81
|
+
|
|
82
|
+
name = shape.option_id()
|
|
83
|
+
|
|
84
|
+
if name in names:
|
|
85
|
+
return path, name
|
|
86
|
+
|
|
87
|
+
names.add(name)
|
|
88
|
+
|
|
89
|
+
for shape in shapes:
|
|
90
|
+
if type(shape) is Struct:
|
|
91
|
+
if id(shape) in seen:
|
|
92
|
+
continue
|
|
93
|
+
|
|
94
|
+
seen.add(id(shape))
|
|
95
|
+
|
|
96
|
+
for field in shape.fields:
|
|
97
|
+
found = _shared_name(field.shape, (*path, field.name), seen)
|
|
98
|
+
|
|
99
|
+
if found is not None:
|
|
100
|
+
return found
|
|
101
|
+
|
|
102
|
+
elif type(shape) is List:
|
|
103
|
+
found = _shared_name(shape.item, path, seen)
|
|
104
|
+
|
|
105
|
+
if found is not None:
|
|
106
|
+
return found
|
|
107
|
+
|
|
108
|
+
return None
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
**La exclusión de `Struct` no estaba en el prompt y es necesaria.** Sin ella la
|
|
112
|
+
guarda se pasaba de estricta y rompía un test verde de la suite anterior
|
|
113
|
+
(`test_codec.py::test_an_enum_sharing_its_name_with_a_dataclass_survives_as_a_member`):
|
|
114
|
+
un dataclass y un enum homónimos **funcionan correctamente**, porque un `Struct`
|
|
115
|
+
lleva su `$type` dentro del objeto y nunca compite por el del wrapper — es
|
|
116
|
+
exactamente lo que arregló la auditoría anterior. La guarda mira solo a las
|
|
117
|
+
opciones que pueden viajar envueltas.
|
|
118
|
+
|
|
119
|
+
Cero privados del core: `option_id()`, `fields`, `field.shape` y `List.item` son
|
|
120
|
+
públicos, y `tests/test_boundary.py` sigue en verde sobre los seis módulos.
|
|
121
|
+
|
|
122
|
+
### Comportamiento, verificado a mano
|
|
123
|
+
|
|
124
|
+
| caso | resultado |
|
|
125
|
+
|---|---|
|
|
126
|
+
| `date \| EnumNamedDate` | `RECHAZA — when: two options of a union share the transport name 'date': rename one of the classes` |
|
|
127
|
+
| el mismo enum **sin** `date` al lado | **ABRE**, y guarda sus miembros |
|
|
128
|
+
| anidado a 3 niveles | `RECHAZA — outer: inner: when: …` (el path completo) |
|
|
129
|
+
| dentro de `list[...]` | `RECHAZA — items: …` |
|
|
130
|
+
| dos enums homónimos | lo rechaza **el core** al compilar (`duplicate discriminator name(s)`) |
|
|
131
|
+
| uniones normales (`str\|date`, `list[str]\|list[int]`, `Enum\|None`) | **ABREN** |
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
## Estado final de los 19 xfail
|
|
136
|
+
|
|
137
|
+
**Seis eran del enum: ninguno queda.** Convertidos en tests del rechazo:
|
|
138
|
+
|
|
139
|
+
| antes (xfail) | ahora |
|
|
140
|
+
|---|---|
|
|
141
|
+
| `unions::test_an_enum_named_str_keeps_its_member_through_the_store` | `test_an_enum_sharing_a_transport_name_with_a_scalar_is_refused_at_open` — mensaje completo por igualdad |
|
|
142
|
+
| `unions::test_a_date_survives_an_enum_named_date_with_an_iso_member` | `test_a_date_beside_an_enum_named_date_is_refused_either_way_round[enum first]` |
|
|
143
|
+
| `unions::test_an_enum_member_named_like_an_iso_date_survives_a_date_option` | el mismo, `[date first]` |
|
|
144
|
+
| `generated::test_enum_named_like_a_scalar_loses_the_member` (codec puro) | `test_an_enum_named_like_a_scalar_is_why_the_store_checks_the_names` — **adaptado, no borrado**: pinta las dos mitades, lo que el codec haría y el rechazo que lo impide, así que quitar la guarda deja un test diciendo qué vuelve en su lugar |
|
|
145
|
+
| `generated::test_enum_named_like_a_scalar_corrupts_a_stored_row` | `test_a_schema_the_codec_could_not_name_never_becomes_a_store` |
|
|
146
|
+
| `generated::test_enum_named_like_date_loses_the_member` | `test_an_enum_named_like_date_is_refused_the_same_way` |
|
|
147
|
+
|
|
148
|
+
Además, dos tests que **pasaban** documentando la mitad superviviente de la
|
|
149
|
+
colisión (`test_an_enum_named_str_takes_a_plain_string_with_it`,
|
|
150
|
+
`test_the_same_collision_refuses_other_values_out_loud`) dejaron de tener
|
|
151
|
+
sentido —el store ya no abre ese schema— y se sustituyeron por
|
|
152
|
+
`test_a_refused_schema_leaves_nothing_behind` y
|
|
153
|
+
`test_an_enum_named_like_a_scalar_it_never_meets_is_fine` (el caso negativo).
|
|
154
|
+
|
|
155
|
+
**Los 13 restantes son todos el mismo límite declarado**, el de la unión de
|
|
156
|
+
listas enrutada por el tipo de los items, ahora con una línea apuntando al
|
|
157
|
+
README en su `reason`:
|
|
158
|
+
|
|
159
|
+
| test | qué fija |
|
|
160
|
+
|---|---|
|
|
161
|
+
| `generated::test_codec_round_trip[seed14, 48, 230, 374]` | los 4 de 500 schemas generados que lo tocan |
|
|
162
|
+
| `generated::test_the_transport_names_the_branch_the_core_would_choose[seed14, 48, 230, 374]` | el `$type` escrito no es la rama que `value_branch` elegiría |
|
|
163
|
+
| `generated::test_store_round_trip[seed14, 48]` | los mismos, a través de un store real |
|
|
164
|
+
| `generated::test_a_union_of_lists_routes_by_type_alone_on_length` | el caso mínimo: `[]` bajo `Annotated[list[str], Min(1)] \| list[int]` |
|
|
165
|
+
| `…_on_a_pattern` | lo mismo con `Pattern` |
|
|
166
|
+
| `…_on_choices` | lo mismo con `Choices` |
|
|
167
|
+
|
|
168
|
+
Ninguno quedó obsoleto por la fase 1: son un defecto distinto, en otra capa.
|
|
169
|
+
|
|
170
|
+
---
|
|
171
|
+
|
|
172
|
+
## Fase 2 — limpieza
|
|
173
|
+
|
|
174
|
+
- **Presupuesto: 29,4 s en frío**, menos de la mitad de los 60 s. Ningún test
|
|
175
|
+
necesitó `@pytest.mark.slow`, así que no se registró el marcador y el README
|
|
176
|
+
no necesita instrucciones aparte. Los cinco más lentos: 2,1 s (4 hilos contra
|
|
177
|
+
el escritor), 1,4 s (bisección del techo de recursión), 1,3 s (5000 filas),
|
|
178
|
+
1,0 s (cobertura del generador), 1,0 s (medición del ritmo de reintento).
|
|
179
|
+
- **Código muerto: no había.** Un barrido AST sobre los 14 archivos de test
|
|
180
|
+
buscando funciones y clases definidas y nunca usadas no encontró ninguna.
|
|
181
|
+
- **mypy limpio en tests**, además de en src. Cinco avisos, todos por
|
|
182
|
+
construcciones deliberadas: tres `Enum("date", …)` guardados en variables con
|
|
183
|
+
otro nombre (es el bug que se está probando), un `v: str | ShadowStr` con una
|
|
184
|
+
variable como tipo, y una anotación que faltaba en `_LIST_VALUES`. Los cuatro
|
|
185
|
+
primeros llevan `# type: ignore` con el código exacto; el quinto se anotó.
|
|
186
|
+
`warn_unused_ignores` está activo, así que si el motivo desaparece, avisa.
|
|
187
|
+
|
|
188
|
+
---
|
|
189
|
+
|
|
190
|
+
## Fase 4 — empaquetado
|
|
191
|
+
|
|
192
|
+
```
|
|
193
|
+
$ python -m build
|
|
194
|
+
Successfully built pytypehintstore-0.0.2.tar.gz and
|
|
195
|
+
pytypehintstore-0.0.2-py3-none-any.whl
|
|
196
|
+
|
|
197
|
+
$ unzip -l dist/pytypehintstore-0.0.2-py3-none-any.whl
|
|
198
|
+
pytypehintstore/__init__.py codec.py errors.py fingerprint.py
|
|
199
|
+
lockfile.py store.py py.typed
|
|
200
|
+
pytypehintstore-0.0.2.dist-info/{METADATA,RECORD,WHEEL,licenses/LICENSE}
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
**El quick start del README, tal cual, contra el wheel en un venv limpio:**
|
|
204
|
+
|
|
205
|
+
```
|
|
206
|
+
$ .venv/Scripts/pip install dist/pytypehintstore-0.0.2-py3-none-any.whl
|
|
207
|
+
pytypehint 0.0.7 · pytypehintstore 0.0.2
|
|
208
|
+
|
|
209
|
+
$ .venv/Scripts/python quickstart.py
|
|
210
|
+
path.name: Task.a50534e4.json ← el mismo nombre que documenta el README
|
|
211
|
+
get: Task(title='Buy milk', priority=<Priority.HIGH: 'high'>, done=False)
|
|
212
|
+
all: [(2, Task(title='Write the store', priority=<Priority.LOW: 'low'>, done=True))]
|
|
213
|
+
reopened: ids kept, next add gave 3
|
|
214
|
+
py.typed viaja: True
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
`example.py` contra ese mismo wheel, tres ejecuciones seguidas: ids 1‑2, 3‑4,
|
|
218
|
+
5‑6, las filas anteriores intactas y las copias rotadas de su familia. El
|
|
219
|
+
directorio acabó con las **dos** bases de datos conviviendo — la `Task` del quick
|
|
220
|
+
start (`a50534e4`) y la `Task` del ejemplo (`975cbdd0`), dos clases con el mismo
|
|
221
|
+
nombre y distinto contrato — que es el contrato hecho visible.
|
|
222
|
+
|
|
223
|
+
---
|
|
224
|
+
|
|
225
|
+
## Decisiones que tomé sin cobertura del prompt
|
|
226
|
+
|
|
227
|
+
1. **La exclusión de `Struct` en la guarda** (arriba). Sin ella, sobre‑rechazo.
|
|
228
|
+
2. **La dependencia se fija en `pytypehint>=0.0.7`**, la versión actual y contra
|
|
229
|
+
la que está probado todo. No bisecté qué versión anterior bastaría: habría
|
|
230
|
+
exigido instalar y correr la suite contra cada una, y el core es del mismo
|
|
231
|
+
autor y avanza junto.
|
|
232
|
+
3. **Las URLs del `pyproject` asumen `github.com/offerrall/pytypehintstore`**,
|
|
233
|
+
por el patrón del resto del ecosistema. **Confírmalas antes de publicar**: si
|
|
234
|
+
el repo se llama de otro modo, son tres líneas.
|
|
235
|
+
4. **La entrada 0.0.1 del CHANGELOG llevaba dos frases falsas** —la firma vieja
|
|
236
|
+
`store_of(cls, path, …)` y el remedio de `missing key(s)`, ambos retirados en
|
|
237
|
+
el rediseño— y las corregí en vez de dejarlas: esa versión nunca se publicó,
|
|
238
|
+
así que su entrada describe la librería, no un histórico.
|
|
239
|
+
5. **`mypy` sigue configurado con `files = ["src"]`**. Los tests pasan si se le
|
|
240
|
+
pide explícitamente (`mypy tests`), pero no los añadí a la configuración: lo
|
|
241
|
+
que se publica es el paquete, y el CI ya corre `mypy src`.
|
|
242
|
+
6. **El test del codec puro del enum se adaptó en vez de eliminarse.** El prompt
|
|
243
|
+
dejaba elegir; conservarlo documenta por qué existe la guarda, y falla con un
|
|
244
|
+
mensaje útil si alguien la quita.
|
|
245
|
+
|
|
246
|
+
---
|
|
247
|
+
|
|
248
|
+
## Lo que queda fuera, a sabiendas
|
|
249
|
+
|
|
250
|
+
- **El límite de la unión de listas** sigue abierto. Arreglarlo necesita el
|
|
251
|
+
router de valores del core, que es privado; la alternativa sería reimplementar
|
|
252
|
+
las restricciones en el codec, es decir, el segundo validador que el diseño
|
|
253
|
+
no quiere. Está en el README, en 13 xfail estrictos y en `INFORME.md`.
|
|
254
|
+
- ~~**POSIX.** La carrera de los dos dueños del lockfile está cerrada en Windows
|
|
255
|
+
porque un archivo abierto no se puede borrar; en POSIX sí.~~ **Cerrado en
|
|
256
|
+
0.0.4**: POSIX toma un `flock` que el kernel suelta al morir el proceso, más
|
|
257
|
+
una comprobación de inodo. La suite corre ahora en `ubuntu-latest` y
|
|
258
|
+
`windows-latest`. Queda una diferencia declarada, con test por plataforma: un
|
|
259
|
+
lockfile que nadie sostiene lo toma POSIX (el `flock` es la evidencia) y lo
|
|
260
|
+
respeta Windows (el PID lo es).
|
|
261
|
+
- **El traceback compartido del segundo `close()`** (cosmético, ya declarado).
|
|
262
|
+
- **`INFORME.md`** se queda en el repo con el detalle de la campaña; dime si
|
|
263
|
+
prefieres que se vaya con este `CIERRE.md`.
|
|
264
|
+
|
|
265
|
+
---
|
|
266
|
+
|
|
267
|
+
## Veredicto
|
|
268
|
+
|
|
269
|
+
La propiedad se sostiene con una excepción, y la excepción es ruidosa: **si el
|
|
270
|
+
core compila el schema, el store lo persiste sin pérdida o falla a gritos** — en
|
|
271
|
+
`add`, o en `store_of` antes de tocar el disco. El único caso de corrupción
|
|
272
|
+
silenciosa que la campaña encontró está cerrado, y lo que queda es un rechazo
|
|
273
|
+
molesto y raro (4 de 500 schemas), no una pérdida.
|
|
274
|
+
|
|
275
|
+
Queda listo para `twine upload`. Ese botón es tuyo.
|