mapwright 0.23.1__tar.gz → 0.24.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.
- {mapwright-0.23.1 → mapwright-0.24.0}/CHANGELOG.md +17 -0
- {mapwright-0.23.1 → mapwright-0.24.0}/PKG-INFO +1 -1
- {mapwright-0.23.1 → mapwright-0.24.0}/pyproject.toml +1 -1
- {mapwright-0.23.1 → mapwright-0.24.0}/src/mapwright/__init__.py +1 -1
- mapwright-0.24.0/src/mapwright/py.typed +0 -0
- mapwright-0.24.0/tests/test_api_contract.py +297 -0
- mapwright-0.23.1/tests/test_api_contract.py +0 -175
- {mapwright-0.23.1 → mapwright-0.24.0}/.github/workflows/ci.yml +0 -0
- {mapwright-0.23.1 → mapwright-0.24.0}/.github/workflows/publish.yml +0 -0
- {mapwright-0.23.1 → mapwright-0.24.0}/.gitignore +0 -0
- {mapwright-0.23.1 → mapwright-0.24.0}/LICENSE +0 -0
- {mapwright-0.23.1 → mapwright-0.24.0}/NOTICE +0 -0
- {mapwright-0.23.1 → mapwright-0.24.0}/README.md +0 -0
- {mapwright-0.23.1 → mapwright-0.24.0}/docs/gallery/age-old.png +0 -0
- {mapwright-0.23.1 → mapwright-0.24.0}/docs/gallery/age-old.svg +0 -0
- {mapwright-0.23.1 → mapwright-0.24.0}/docs/gallery/age-young.png +0 -0
- {mapwright-0.23.1 → mapwright-0.24.0}/docs/gallery/age-young.svg +0 -0
- {mapwright-0.23.1 → mapwright-0.24.0}/docs/gallery/archipelago.png +0 -0
- {mapwright-0.23.1 → mapwright-0.24.0}/docs/gallery/archipelago.svg +0 -0
- {mapwright-0.23.1 → mapwright-0.24.0}/docs/gallery/arctic.png +0 -0
- {mapwright-0.23.1 → mapwright-0.24.0}/docs/gallery/arctic.svg +0 -0
- {mapwright-0.23.1 → mapwright-0.24.0}/docs/gallery/atlas.png +0 -0
- {mapwright-0.23.1 → mapwright-0.24.0}/docs/gallery/atlas_pack/README.md +0 -0
- {mapwright-0.23.1 → mapwright-0.24.0}/docs/gallery/atlas_pack/city_castle_1.png +0 -0
- {mapwright-0.23.1 → mapwright-0.24.0}/docs/gallery/atlas_pack/city_large_1.png +0 -0
- {mapwright-0.23.1 → mapwright-0.24.0}/docs/gallery/atlas_pack/city_town_1.png +0 -0
- {mapwright-0.23.1 → mapwright-0.24.0}/docs/gallery/atlas_pack/city_village_1.png +0 -0
- {mapwright-0.23.1 → mapwright-0.24.0}/docs/gallery/atlas_pack/decoration_compass_1.png +0 -0
- {mapwright-0.23.1 → mapwright-0.24.0}/docs/gallery/atlas_pack/decoration_creature_1.png +0 -0
- {mapwright-0.23.1 → mapwright-0.24.0}/docs/gallery/atlas_pack/decoration_ship_1.png +0 -0
- {mapwright-0.23.1 → mapwright-0.24.0}/docs/gallery/atlas_pack/dune_1.png +0 -0
- {mapwright-0.23.1 → mapwright-0.24.0}/docs/gallery/atlas_pack/hill_1.png +0 -0
- {mapwright-0.23.1 → mapwright-0.24.0}/docs/gallery/atlas_pack/hill_2.png +0 -0
- {mapwright-0.23.1 → mapwright-0.24.0}/docs/gallery/atlas_pack/manifest.json +0 -0
- {mapwright-0.23.1 → mapwright-0.24.0}/docs/gallery/atlas_pack/mountain_mid_1.png +0 -0
- {mapwright-0.23.1 → mapwright-0.24.0}/docs/gallery/atlas_pack/mountain_old_1.png +0 -0
- {mapwright-0.23.1 → mapwright-0.24.0}/docs/gallery/atlas_pack/mountain_old_2.png +0 -0
- {mapwright-0.23.1 → mapwright-0.24.0}/docs/gallery/atlas_pack/mountain_young_1.png +0 -0
- {mapwright-0.23.1 → mapwright-0.24.0}/docs/gallery/atlas_pack/mountain_young_2.png +0 -0
- {mapwright-0.23.1 → mapwright-0.24.0}/docs/gallery/atlas_pack/tree_cactus_1.png +0 -0
- {mapwright-0.23.1 → mapwright-0.24.0}/docs/gallery/atlas_pack/tree_deciduous_1.png +0 -0
- {mapwright-0.23.1 → mapwright-0.24.0}/docs/gallery/atlas_pack/tree_deciduous_2.png +0 -0
- {mapwright-0.23.1 → mapwright-0.24.0}/docs/gallery/atlas_pack/tree_pine_1.png +0 -0
- {mapwright-0.23.1 → mapwright-0.24.0}/docs/gallery/atlas_pack/tree_pine_2.png +0 -0
- {mapwright-0.23.1 → mapwright-0.24.0}/docs/gallery/citadel.png +0 -0
- {mapwright-0.23.1 → mapwright-0.24.0}/docs/gallery/citadel.svg +0 -0
- {mapwright-0.23.1 → mapwright-0.24.0}/docs/gallery/continent.png +0 -0
- {mapwright-0.23.1 → mapwright-0.24.0}/docs/gallery/continent.svg +0 -0
- {mapwright-0.23.1 → mapwright-0.24.0}/docs/gallery/desert.png +0 -0
- {mapwright-0.23.1 → mapwright-0.24.0}/docs/gallery/desert.svg +0 -0
- {mapwright-0.23.1 → mapwright-0.24.0}/docs/gallery/dungeon.png +0 -0
- {mapwright-0.23.1 → mapwright-0.24.0}/docs/gallery/dungeon.svg +0 -0
- {mapwright-0.23.1 → mapwright-0.24.0}/docs/gallery/fortress-town.png +0 -0
- {mapwright-0.23.1 → mapwright-0.24.0}/docs/gallery/fortress-town.svg +0 -0
- {mapwright-0.23.1 → mapwright-0.24.0}/docs/gallery/grid-city.png +0 -0
- {mapwright-0.23.1 → mapwright-0.24.0}/docs/gallery/grid-city.svg +0 -0
- {mapwright-0.23.1 → mapwright-0.24.0}/docs/gallery/highlands.png +0 -0
- {mapwright-0.23.1 → mapwright-0.24.0}/docs/gallery/highlands.svg +0 -0
- {mapwright-0.23.1 → mapwright-0.24.0}/docs/gallery/hint.png +0 -0
- {mapwright-0.23.1 → mapwright-0.24.0}/docs/gallery/hint.svg +0 -0
- {mapwright-0.23.1 → mapwright-0.24.0}/docs/gallery/islands.png +0 -0
- {mapwright-0.23.1 → mapwright-0.24.0}/docs/gallery/islands.svg +0 -0
- {mapwright-0.23.1 → mapwright-0.24.0}/docs/gallery/metropolis.png +0 -0
- {mapwright-0.23.1 → mapwright-0.24.0}/docs/gallery/metropolis.svg +0 -0
- {mapwright-0.23.1 → mapwright-0.24.0}/docs/gallery/pangaea.png +0 -0
- {mapwright-0.23.1 → mapwright-0.24.0}/docs/gallery/pangaea.svg +0 -0
- {mapwright-0.23.1 → mapwright-0.24.0}/docs/gallery/port.png +0 -0
- {mapwright-0.23.1 → mapwright-0.24.0}/docs/gallery/port.svg +0 -0
- {mapwright-0.23.1 → mapwright-0.24.0}/docs/gallery/regions.png +0 -0
- {mapwright-0.23.1 → mapwright-0.24.0}/docs/gallery/regions.svg +0 -0
- {mapwright-0.23.1 → mapwright-0.24.0}/docs/gallery/roads.png +0 -0
- {mapwright-0.23.1 → mapwright-0.24.0}/docs/gallery/roads.svg +0 -0
- {mapwright-0.23.1 → mapwright-0.24.0}/docs/gallery/shantytown.png +0 -0
- {mapwright-0.23.1 → mapwright-0.24.0}/docs/gallery/shantytown.svg +0 -0
- {mapwright-0.23.1 → mapwright-0.24.0}/docs/gallery/template-atoll.png +0 -0
- {mapwright-0.23.1 → mapwright-0.24.0}/docs/gallery/template-atoll.svg +0 -0
- {mapwright-0.23.1 → mapwright-0.24.0}/docs/gallery/template-isthmus.png +0 -0
- {mapwright-0.23.1 → mapwright-0.24.0}/docs/gallery/template-isthmus.svg +0 -0
- {mapwright-0.23.1 → mapwright-0.24.0}/docs/gallery/terrain-town.png +0 -0
- {mapwright-0.23.1 → mapwright-0.24.0}/docs/gallery/terrain-town.svg +0 -0
- {mapwright-0.23.1 → mapwright-0.24.0}/docs/gallery/theme-blueprint.png +0 -0
- {mapwright-0.23.1 → mapwright-0.24.0}/docs/gallery/theme-blueprint.svg +0 -0
- {mapwright-0.23.1 → mapwright-0.24.0}/docs/gallery/theme-citadel-neon.png +0 -0
- {mapwright-0.23.1 → mapwright-0.24.0}/docs/gallery/theme-citadel-neon.svg +0 -0
- {mapwright-0.23.1 → mapwright-0.24.0}/docs/gallery/theme-dune.png +0 -0
- {mapwright-0.23.1 → mapwright-0.24.0}/docs/gallery/theme-dune.svg +0 -0
- {mapwright-0.23.1 → mapwright-0.24.0}/docs/gallery/theme-dungeon-blueprint.png +0 -0
- {mapwright-0.23.1 → mapwright-0.24.0}/docs/gallery/theme-dungeon-blueprint.svg +0 -0
- {mapwright-0.23.1 → mapwright-0.24.0}/docs/gallery/theme-neon.png +0 -0
- {mapwright-0.23.1 → mapwright-0.24.0}/docs/gallery/theme-neon.svg +0 -0
- {mapwright-0.23.1 → mapwright-0.24.0}/docs/gallery/theme-parchment.png +0 -0
- {mapwright-0.23.1 → mapwright-0.24.0}/docs/gallery/theme-parchment.svg +0 -0
- {mapwright-0.23.1 → mapwright-0.24.0}/docs/gallery/town.png +0 -0
- {mapwright-0.23.1 → mapwright-0.24.0}/docs/gallery/town.svg +0 -0
- {mapwright-0.23.1 → mapwright-0.24.0}/docs/gallery/tropical.png +0 -0
- {mapwright-0.23.1 → mapwright-0.24.0}/docs/gallery/tropical.svg +0 -0
- {mapwright-0.23.1 → mapwright-0.24.0}/examples/benchmark.py +0 -0
- {mapwright-0.23.1 → mapwright-0.24.0}/examples/gallery.py +0 -0
- {mapwright-0.23.1 → mapwright-0.24.0}/src/mapwright/_geometry.py +0 -0
- {mapwright-0.23.1 → mapwright-0.24.0}/src/mapwright/_graph.py +0 -0
- {mapwright-0.23.1 → mapwright-0.24.0}/src/mapwright/_serde.py +0 -0
- {mapwright-0.23.1 → mapwright-0.24.0}/src/mapwright/affordances.py +0 -0
- {mapwright-0.23.1 → mapwright-0.24.0}/src/mapwright/atlas_renderer.py +0 -0
- {mapwright-0.23.1 → mapwright-0.24.0}/src/mapwright/config.py +0 -0
- {mapwright-0.23.1 → mapwright-0.24.0}/src/mapwright/dungeon.py +0 -0
- {mapwright-0.23.1 → mapwright-0.24.0}/src/mapwright/dungeon_renderer.py +0 -0
- {mapwright-0.23.1 → mapwright-0.24.0}/src/mapwright/names.py +0 -0
- {mapwright-0.23.1 → mapwright-0.24.0}/src/mapwright/regions.py +0 -0
- {mapwright-0.23.1 → mapwright-0.24.0}/src/mapwright/rng.py +0 -0
- {mapwright-0.23.1 → mapwright-0.24.0}/src/mapwright/roads.py +0 -0
- {mapwright-0.23.1 → mapwright-0.24.0}/src/mapwright/settlement.py +0 -0
- {mapwright-0.23.1 → mapwright-0.24.0}/src/mapwright/settlement_renderer.py +0 -0
- {mapwright-0.23.1 → mapwright-0.24.0}/src/mapwright/svg_renderer.py +0 -0
- {mapwright-0.23.1 → mapwright-0.24.0}/src/mapwright/terrain.py +0 -0
- {mapwright-0.23.1 → mapwright-0.24.0}/src/mapwright/themes.py +0 -0
- {mapwright-0.23.1 → mapwright-0.24.0}/tests/test_affordances.py +0 -0
- {mapwright-0.23.1 → mapwright-0.24.0}/tests/test_atlas_renderer.py +0 -0
- {mapwright-0.23.1 → mapwright-0.24.0}/tests/test_config.py +0 -0
- {mapwright-0.23.1 → mapwright-0.24.0}/tests/test_dungeon.py +0 -0
- {mapwright-0.23.1 → mapwright-0.24.0}/tests/test_dungeon_renderer.py +0 -0
- {mapwright-0.23.1 → mapwright-0.24.0}/tests/test_geometry.py +0 -0
- {mapwright-0.23.1 → mapwright-0.24.0}/tests/test_graph.py +0 -0
- {mapwright-0.23.1 → mapwright-0.24.0}/tests/test_names.py +0 -0
- {mapwright-0.23.1 → mapwright-0.24.0}/tests/test_properties.py +0 -0
- {mapwright-0.23.1 → mapwright-0.24.0}/tests/test_regions.py +0 -0
- {mapwright-0.23.1 → mapwright-0.24.0}/tests/test_rng.py +0 -0
- {mapwright-0.23.1 → mapwright-0.24.0}/tests/test_roads.py +0 -0
- {mapwright-0.23.1 → mapwright-0.24.0}/tests/test_serialize.py +0 -0
- {mapwright-0.23.1 → mapwright-0.24.0}/tests/test_settlement.py +0 -0
- {mapwright-0.23.1 → mapwright-0.24.0}/tests/test_svg_renderer.py +0 -0
- {mapwright-0.23.1 → mapwright-0.24.0}/tests/test_terrain.py +0 -0
- {mapwright-0.23.1 → mapwright-0.24.0}/tests/test_themes.py +0 -0
|
@@ -8,6 +8,23 @@ All notable changes to mapwright are documented here. The format follows
|
|
|
8
8
|
`tests/test_api_contract.py`). While the version is `0.x`, minor versions may
|
|
9
9
|
make breaking changes; these will always be noted here.
|
|
10
10
|
|
|
11
|
+
## [0.24.0] — 2026-06-06
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
- **Type information (PEP 561).** Ships a `py.typed` marker, so downstream
|
|
15
|
+
projects' type checkers (mypy, pyright) now verify their use of mapwright's
|
|
16
|
+
public API against its annotations — signature drift is caught at *their*
|
|
17
|
+
build time, not just at runtime.
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
- **The public contract is now pinned more tightly** (no API change). The
|
|
21
|
+
contract tests freeze, for every exported dataclass, the exact field *names
|
|
22
|
+
and order* (positional construction, attribute access, and `to_dict` keys all
|
|
23
|
+
depend on these), and freeze the `to_dict()` key schema for every serialisable
|
|
24
|
+
type. An internal refactor that renames/reorders/drops a public field or
|
|
25
|
+
changes a serialised key now fails CI loudly instead of silently breaking
|
|
26
|
+
consumers that persist JSON or read fields.
|
|
27
|
+
|
|
11
28
|
## [0.23.1] — 2026-06-06
|
|
12
29
|
|
|
13
30
|
### Fixed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mapwright
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.24.0
|
|
4
4
|
Summary: Domain-neutral procedural fantasy map & world generation: Voronoi terrain, hydraulic erosion, biomes, rivers, Markov place-names, and shaded-relief SVG.
|
|
5
5
|
Project-URL: Homepage, https://github.com/sligara7/mapwright
|
|
6
6
|
Project-URL: Repository, https://github.com/sligara7/mapwright
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "mapwright"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.24.0"
|
|
8
8
|
description = "Domain-neutral procedural fantasy map & world generation: Voronoi terrain, hydraulic erosion, biomes, rivers, Markov place-names, and shaded-relief SVG."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
File without changes
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
"""API contract tests — the public surface other code may rely on.
|
|
2
|
+
|
|
3
|
+
These pin the public API so a breaking change fails loudly (and CI catches it).
|
|
4
|
+
If a change here is intentional, update the expected sets *and* the version /
|
|
5
|
+
CHANGELOG per semver.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import dataclasses
|
|
9
|
+
import inspect
|
|
10
|
+
|
|
11
|
+
import mapwright
|
|
12
|
+
from mapwright import WorldMapConfig
|
|
13
|
+
from mapwright.config import _SPEC
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# The frozen public surface (mapwright.__all__). Adding is a minor bump;
|
|
17
|
+
# removing/renaming is a breaking (major, pre-1.0: minor) change.
|
|
18
|
+
EXPECTED_PUBLIC = {
|
|
19
|
+
"SeededRNG",
|
|
20
|
+
"WorldMapConfig",
|
|
21
|
+
"PRESETS",
|
|
22
|
+
"CellSummary",
|
|
23
|
+
"environment_affordances",
|
|
24
|
+
"summarize_cells",
|
|
25
|
+
"NameGenerator",
|
|
26
|
+
"MarkovNameGenerator",
|
|
27
|
+
"NAMEBASES",
|
|
28
|
+
"Biome",
|
|
29
|
+
"River",
|
|
30
|
+
"TerrainCell",
|
|
31
|
+
"TerrainResult",
|
|
32
|
+
"RegionalTerrainGenerator",
|
|
33
|
+
"TERRAIN_TEMPLATES",
|
|
34
|
+
"compute_cell_polygons",
|
|
35
|
+
"Marker",
|
|
36
|
+
"RegionalSVGRenderer",
|
|
37
|
+
"Theme",
|
|
38
|
+
"THEMES",
|
|
39
|
+
"ArtPack",
|
|
40
|
+
"AtlasRenderer",
|
|
41
|
+
"Road",
|
|
42
|
+
"RegionalRoadGenerator",
|
|
43
|
+
"Region",
|
|
44
|
+
"RegionGenerator",
|
|
45
|
+
"Dungeon",
|
|
46
|
+
"DungeonConfig",
|
|
47
|
+
"DungeonGenerator",
|
|
48
|
+
"DungeonSVGRenderer",
|
|
49
|
+
"Rect",
|
|
50
|
+
"Settlement",
|
|
51
|
+
"SettlementConfig",
|
|
52
|
+
"SettlementGenerator",
|
|
53
|
+
"SettlementSVGRenderer",
|
|
54
|
+
"Ward",
|
|
55
|
+
"Lot",
|
|
56
|
+
"Street",
|
|
57
|
+
"Wall",
|
|
58
|
+
"Landmark",
|
|
59
|
+
"TerrainField",
|
|
60
|
+
"world_terrain_field",
|
|
61
|
+
"SETTLEMENT_PRESETS",
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
# The frozen field layout of every exported dataclass. Field NAMES *and ORDER*
|
|
66
|
+
# are part of the public contract: positional construction, attribute access,
|
|
67
|
+
# and the keys emitted by ``to_dict`` all depend on them. Appending a trailing
|
|
68
|
+
# optional field is a minor bump; renaming/removing/reordering is breaking.
|
|
69
|
+
EXPECTED_FIELDS = {
|
|
70
|
+
"WorldMapConfig": ("sea_level", "continents", "continent_spread",
|
|
71
|
+
"edge_falloff", "mountain_density", "roughness",
|
|
72
|
+
"land_age", "temperature", "moisture", "river_density",
|
|
73
|
+
"lake_density"),
|
|
74
|
+
"CellSummary": ("dominant_biome", "temperature", "moisture", "mean_height",
|
|
75
|
+
"has_river", "has_lake", "water_fraction", "cell_count",
|
|
76
|
+
"affordances"),
|
|
77
|
+
"TerrainCell": ("id", "cx", "cy", "neighbors", "height", "filled", "flux",
|
|
78
|
+
"downhill", "is_water", "is_lake", "is_river", "temperature",
|
|
79
|
+
"moisture", "biome"),
|
|
80
|
+
"River": ("cells", "width"),
|
|
81
|
+
"TerrainResult": ("width", "height", "cells", "cell_of", "rivers",
|
|
82
|
+
"sea_level"),
|
|
83
|
+
"Marker": ("name", "x", "y", "kind"),
|
|
84
|
+
"Theme": ("name", "biomes", "ocean_bg", "coastline", "river", "road",
|
|
85
|
+
"road_casing", "region_border", "region_label", "settlement_fill",
|
|
86
|
+
"settlement_stroke", "label_fill", "label_halo", "biome_names",
|
|
87
|
+
"settlement", "dungeon"),
|
|
88
|
+
"ArtPack": ("slots", "colors", "name"),
|
|
89
|
+
"Road": ("cells",),
|
|
90
|
+
"Region": ("id", "name", "capital", "cells"),
|
|
91
|
+
"Dungeon": ("width", "height", "rooms", "corridors", "grid", "edges"),
|
|
92
|
+
"DungeonConfig": ("min_leaf", "room_min", "room_padding", "split_jitter",
|
|
93
|
+
"extra_corridor_chance"),
|
|
94
|
+
"Rect": ("x", "y", "w", "h"),
|
|
95
|
+
"Settlement": ("width", "height", "name", "footprint", "wards", "lots",
|
|
96
|
+
"streets", "gates", "wall", "landmark", "walled", "coastal",
|
|
97
|
+
"purpose", "water_edge"),
|
|
98
|
+
"SettlementConfig": ("population", "irregularity", "lot_size", "wealth",
|
|
99
|
+
"era", "layout", "purpose", "walled", "coastal"),
|
|
100
|
+
"Ward": ("id", "polygon", "center", "name", "kind"),
|
|
101
|
+
"Lot": ("id", "polygon", "ward"),
|
|
102
|
+
"Street": ("path", "kind"),
|
|
103
|
+
"Wall": ("ring", "closed", "gates"),
|
|
104
|
+
"Landmark": ("ward", "kind", "center", "name"),
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
# Serialisable types whose ``to_dict`` emits an extra ``"schema"`` version tag
|
|
108
|
+
# on top of their field keys (the three top-level documents).
|
|
109
|
+
SCHEMA_TAGGED = {"TerrainResult", "Dungeon", "Settlement"}
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class TestPublicSurface:
|
|
113
|
+
def test_all_matches_contract(self):
|
|
114
|
+
assert set(mapwright.__all__) == EXPECTED_PUBLIC
|
|
115
|
+
|
|
116
|
+
def test_everything_in_all_is_importable(self):
|
|
117
|
+
for name in mapwright.__all__:
|
|
118
|
+
assert hasattr(mapwright, name), f"{name} missing from package"
|
|
119
|
+
|
|
120
|
+
def test_version_is_present(self):
|
|
121
|
+
assert isinstance(mapwright.__version__, str)
|
|
122
|
+
assert mapwright.__version__.count(".") >= 2 # semver-ish
|
|
123
|
+
|
|
124
|
+
def test_version_matches_package_metadata(self):
|
|
125
|
+
# __version__ must match the installed (pyproject) version, so a missed
|
|
126
|
+
# bump can't ship a mislabelled wheel. Skips when run from source.
|
|
127
|
+
import importlib.metadata as md
|
|
128
|
+
|
|
129
|
+
try:
|
|
130
|
+
installed = md.version("mapwright")
|
|
131
|
+
except md.PackageNotFoundError:
|
|
132
|
+
import pytest
|
|
133
|
+
|
|
134
|
+
pytest.skip("mapwright not installed; running from source tree")
|
|
135
|
+
assert installed == mapwright.__version__
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class TestKeySignatures:
|
|
139
|
+
def test_generate_signature(self):
|
|
140
|
+
params = inspect.signature(
|
|
141
|
+
mapwright.RegionalTerrainGenerator.generate
|
|
142
|
+
).parameters
|
|
143
|
+
assert ["self", "width", "height", "config"] == list(params)[:4]
|
|
144
|
+
assert params["config"].default is None
|
|
145
|
+
|
|
146
|
+
def test_svg_render_signature(self):
|
|
147
|
+
params = inspect.signature(mapwright.RegionalSVGRenderer.render).parameters
|
|
148
|
+
assert ["self", "terrain", "markers"] == list(params)[:3]
|
|
149
|
+
|
|
150
|
+
def test_marker_fields(self):
|
|
151
|
+
fields = {f.name for f in dataclasses.fields(mapwright.Marker)}
|
|
152
|
+
assert {"name", "x", "y", "kind"} <= fields
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
class TestSerialisationContract:
|
|
156
|
+
"""The (de)serialisation surface is part of the public contract."""
|
|
157
|
+
|
|
158
|
+
def test_roundtrip_types_have_dict_methods(self):
|
|
159
|
+
for cls in (
|
|
160
|
+
mapwright.TerrainResult,
|
|
161
|
+
mapwright.Dungeon,
|
|
162
|
+
mapwright.Marker,
|
|
163
|
+
mapwright.TerrainCell,
|
|
164
|
+
mapwright.River,
|
|
165
|
+
mapwright.Rect,
|
|
166
|
+
mapwright.Road,
|
|
167
|
+
mapwright.Region,
|
|
168
|
+
mapwright.Settlement,
|
|
169
|
+
mapwright.Ward,
|
|
170
|
+
mapwright.Lot,
|
|
171
|
+
mapwright.Street,
|
|
172
|
+
mapwright.Wall,
|
|
173
|
+
mapwright.Landmark,
|
|
174
|
+
mapwright.SettlementConfig,
|
|
175
|
+
):
|
|
176
|
+
assert hasattr(cls, "to_dict") and callable(cls.to_dict)
|
|
177
|
+
assert hasattr(cls, "from_dict") and callable(cls.from_dict)
|
|
178
|
+
|
|
179
|
+
def test_top_level_types_have_json_methods(self):
|
|
180
|
+
for cls in (mapwright.TerrainResult, mapwright.Dungeon, mapwright.Marker,
|
|
181
|
+
mapwright.Settlement):
|
|
182
|
+
assert hasattr(cls, "to_json") and callable(cls.to_json)
|
|
183
|
+
assert hasattr(cls, "from_json") and callable(cls.from_json)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
class TestConfigContract:
|
|
187
|
+
def test_spec_covers_every_field_exactly(self):
|
|
188
|
+
spec_names = {name for name, *_ in _SPEC}
|
|
189
|
+
field_names = {f.name for f in dataclasses.fields(WorldMapConfig)}
|
|
190
|
+
assert spec_names == field_names
|
|
191
|
+
|
|
192
|
+
def test_json_schema_shape(self):
|
|
193
|
+
schema = WorldMapConfig.json_schema()
|
|
194
|
+
assert schema["type"] == "object"
|
|
195
|
+
assert schema["additionalProperties"] is False
|
|
196
|
+
props = schema["properties"]
|
|
197
|
+
assert set(props) == {f.name for f in dataclasses.fields(WorldMapConfig)}
|
|
198
|
+
|
|
199
|
+
def test_json_schema_bounds_and_defaults_match(self):
|
|
200
|
+
schema = WorldMapConfig.json_schema()
|
|
201
|
+
defaults = WorldMapConfig()
|
|
202
|
+
for name, typ, lo, hi, _desc in _SPEC:
|
|
203
|
+
p = schema["properties"][name]
|
|
204
|
+
assert p["type"] == ("integer" if typ is int else "number")
|
|
205
|
+
assert p["minimum"] == lo and p["maximum"] == hi
|
|
206
|
+
assert p["default"] == getattr(defaults, name)
|
|
207
|
+
|
|
208
|
+
def test_schema_bounds_are_actually_enforced(self):
|
|
209
|
+
# A payload that violates the schema bounds is clamped into them.
|
|
210
|
+
for name, _typ, lo, hi, _desc in _SPEC:
|
|
211
|
+
below = WorldMapConfig.from_dict({name: lo - 100})
|
|
212
|
+
above = WorldMapConfig.from_dict({name: hi + 100})
|
|
213
|
+
assert getattr(below, name) >= lo
|
|
214
|
+
assert getattr(above, name) <= hi
|
|
215
|
+
|
|
216
|
+
def test_presets_are_valid_against_schema(self):
|
|
217
|
+
# Every preset must only use known keys within range (from_dict clamps,
|
|
218
|
+
# but presets should already be in-bounds by construction).
|
|
219
|
+
for name in WorldMapConfig.preset_names():
|
|
220
|
+
cfg = WorldMapConfig.preset(name)
|
|
221
|
+
for fname, _typ, lo, hi, _desc in _SPEC:
|
|
222
|
+
assert lo <= getattr(cfg, fname) <= hi
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
class TestDataclassLayout:
|
|
226
|
+
"""Field names + order of every exported dataclass are frozen (see
|
|
227
|
+
``EXPECTED_FIELDS``). This is what consumers rely on for positional
|
|
228
|
+
construction, attribute access, and serialisation key stability."""
|
|
229
|
+
|
|
230
|
+
def test_every_exported_dataclass_is_pinned(self):
|
|
231
|
+
for name in mapwright.__all__:
|
|
232
|
+
obj = getattr(mapwright, name)
|
|
233
|
+
if not (isinstance(obj, type) and dataclasses.is_dataclass(obj)):
|
|
234
|
+
continue
|
|
235
|
+
assert name in EXPECTED_FIELDS, (
|
|
236
|
+
f"new exported dataclass {name!r} — add it to EXPECTED_FIELDS "
|
|
237
|
+
f"(and bump the version / note it in the CHANGELOG)"
|
|
238
|
+
)
|
|
239
|
+
actual = tuple(f.name for f in dataclasses.fields(obj))
|
|
240
|
+
assert actual == EXPECTED_FIELDS[name], (
|
|
241
|
+
f"{name} field layout changed: {actual} != {EXPECTED_FIELDS[name]}"
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
def test_no_stale_entries_in_expected_fields(self):
|
|
245
|
+
# Every pinned name must still be an exported dataclass.
|
|
246
|
+
for name in EXPECTED_FIELDS:
|
|
247
|
+
obj = getattr(mapwright, name, None)
|
|
248
|
+
assert obj is not None and dataclasses.is_dataclass(obj), (
|
|
249
|
+
f"{name} is pinned in EXPECTED_FIELDS but no longer an exported dataclass"
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
class TestToDictSchema:
|
|
254
|
+
"""``to_dict()`` must emit exactly the pinned field keys (plus a ``schema``
|
|
255
|
+
tag on the top-level documents) — so a consumer persisting JSON is protected
|
|
256
|
+
against a silent key rename/drop that leaves the dataclass field untouched."""
|
|
257
|
+
|
|
258
|
+
def _instances(self):
|
|
259
|
+
from mapwright import (
|
|
260
|
+
SeededRNG, RegionalTerrainGenerator, DungeonGenerator, DungeonConfig,
|
|
261
|
+
SettlementGenerator, SettlementConfig, Marker, River, Road, Region,
|
|
262
|
+
Landmark,
|
|
263
|
+
)
|
|
264
|
+
terrain = RegionalTerrainGenerator(SeededRNG(7)).generate(60, 45)
|
|
265
|
+
dungeon = DungeonGenerator(SeededRNG(7)).generate(48, 40, DungeonConfig())
|
|
266
|
+
town = SettlementGenerator(SeededRNG(3)).generate(
|
|
267
|
+
900, 700, SettlementConfig(population=12000, walled=True)
|
|
268
|
+
)
|
|
269
|
+
assert town.wards and town.lots and town.streets and town.wall, (
|
|
270
|
+
"test fixture must produce a town with wards/lots/streets/wall"
|
|
271
|
+
)
|
|
272
|
+
return {
|
|
273
|
+
"TerrainResult": terrain,
|
|
274
|
+
"TerrainCell": terrain.cells[0],
|
|
275
|
+
"River": River([0, 1], 1.0),
|
|
276
|
+
"Marker": Marker(name="X", x=1.0, y=2.0, kind="city"),
|
|
277
|
+
"Dungeon": dungeon,
|
|
278
|
+
"Rect": dungeon.rooms[0],
|
|
279
|
+
"Road": Road([0, 1]),
|
|
280
|
+
"Region": Region(id=0, name="R", capital=5, cells=[1, 2, 3]),
|
|
281
|
+
"Settlement": town,
|
|
282
|
+
"Ward": town.wards[0],
|
|
283
|
+
"Lot": town.lots[0],
|
|
284
|
+
"Street": town.streets[0],
|
|
285
|
+
"Wall": town.wall,
|
|
286
|
+
"Landmark": Landmark(ward=0, kind="temple", center=(1.0, 2.0), name="Shrine"),
|
|
287
|
+
"SettlementConfig": SettlementConfig(),
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
def test_to_dict_keys_match_contract(self):
|
|
291
|
+
for name, inst in self._instances().items():
|
|
292
|
+
expected = set(EXPECTED_FIELDS[name])
|
|
293
|
+
if name in SCHEMA_TAGGED:
|
|
294
|
+
expected |= {"schema"}
|
|
295
|
+
assert set(inst.to_dict()) == expected, (
|
|
296
|
+
f"{name}.to_dict() keys drifted: {set(inst.to_dict())} != {expected}"
|
|
297
|
+
)
|
|
@@ -1,175 +0,0 @@
|
|
|
1
|
-
"""API contract tests — the public surface other code may rely on.
|
|
2
|
-
|
|
3
|
-
These pin the public API so a breaking change fails loudly (and CI catches it).
|
|
4
|
-
If a change here is intentional, update the expected sets *and* the version /
|
|
5
|
-
CHANGELOG per semver.
|
|
6
|
-
"""
|
|
7
|
-
|
|
8
|
-
import dataclasses
|
|
9
|
-
import inspect
|
|
10
|
-
|
|
11
|
-
import mapwright
|
|
12
|
-
from mapwright import WorldMapConfig
|
|
13
|
-
from mapwright.config import _SPEC
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
# The frozen public surface (mapwright.__all__). Adding is a minor bump;
|
|
17
|
-
# removing/renaming is a breaking (major, pre-1.0: minor) change.
|
|
18
|
-
EXPECTED_PUBLIC = {
|
|
19
|
-
"SeededRNG",
|
|
20
|
-
"WorldMapConfig",
|
|
21
|
-
"PRESETS",
|
|
22
|
-
"CellSummary",
|
|
23
|
-
"environment_affordances",
|
|
24
|
-
"summarize_cells",
|
|
25
|
-
"NameGenerator",
|
|
26
|
-
"MarkovNameGenerator",
|
|
27
|
-
"NAMEBASES",
|
|
28
|
-
"Biome",
|
|
29
|
-
"River",
|
|
30
|
-
"TerrainCell",
|
|
31
|
-
"TerrainResult",
|
|
32
|
-
"RegionalTerrainGenerator",
|
|
33
|
-
"TERRAIN_TEMPLATES",
|
|
34
|
-
"compute_cell_polygons",
|
|
35
|
-
"Marker",
|
|
36
|
-
"RegionalSVGRenderer",
|
|
37
|
-
"Theme",
|
|
38
|
-
"THEMES",
|
|
39
|
-
"ArtPack",
|
|
40
|
-
"AtlasRenderer",
|
|
41
|
-
"Road",
|
|
42
|
-
"RegionalRoadGenerator",
|
|
43
|
-
"Region",
|
|
44
|
-
"RegionGenerator",
|
|
45
|
-
"Dungeon",
|
|
46
|
-
"DungeonConfig",
|
|
47
|
-
"DungeonGenerator",
|
|
48
|
-
"DungeonSVGRenderer",
|
|
49
|
-
"Rect",
|
|
50
|
-
"Settlement",
|
|
51
|
-
"SettlementConfig",
|
|
52
|
-
"SettlementGenerator",
|
|
53
|
-
"SettlementSVGRenderer",
|
|
54
|
-
"Ward",
|
|
55
|
-
"Lot",
|
|
56
|
-
"Street",
|
|
57
|
-
"Wall",
|
|
58
|
-
"Landmark",
|
|
59
|
-
"TerrainField",
|
|
60
|
-
"world_terrain_field",
|
|
61
|
-
"SETTLEMENT_PRESETS",
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
class TestPublicSurface:
|
|
66
|
-
def test_all_matches_contract(self):
|
|
67
|
-
assert set(mapwright.__all__) == EXPECTED_PUBLIC
|
|
68
|
-
|
|
69
|
-
def test_everything_in_all_is_importable(self):
|
|
70
|
-
for name in mapwright.__all__:
|
|
71
|
-
assert hasattr(mapwright, name), f"{name} missing from package"
|
|
72
|
-
|
|
73
|
-
def test_version_is_present(self):
|
|
74
|
-
assert isinstance(mapwright.__version__, str)
|
|
75
|
-
assert mapwright.__version__.count(".") >= 2 # semver-ish
|
|
76
|
-
|
|
77
|
-
def test_version_matches_package_metadata(self):
|
|
78
|
-
# __version__ must match the installed (pyproject) version, so a missed
|
|
79
|
-
# bump can't ship a mislabelled wheel. Skips when run from source.
|
|
80
|
-
import importlib.metadata as md
|
|
81
|
-
|
|
82
|
-
try:
|
|
83
|
-
installed = md.version("mapwright")
|
|
84
|
-
except md.PackageNotFoundError:
|
|
85
|
-
import pytest
|
|
86
|
-
|
|
87
|
-
pytest.skip("mapwright not installed; running from source tree")
|
|
88
|
-
assert installed == mapwright.__version__
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
class TestKeySignatures:
|
|
92
|
-
def test_generate_signature(self):
|
|
93
|
-
params = inspect.signature(
|
|
94
|
-
mapwright.RegionalTerrainGenerator.generate
|
|
95
|
-
).parameters
|
|
96
|
-
assert ["self", "width", "height", "config"] == list(params)[:4]
|
|
97
|
-
assert params["config"].default is None
|
|
98
|
-
|
|
99
|
-
def test_svg_render_signature(self):
|
|
100
|
-
params = inspect.signature(mapwright.RegionalSVGRenderer.render).parameters
|
|
101
|
-
assert ["self", "terrain", "markers"] == list(params)[:3]
|
|
102
|
-
|
|
103
|
-
def test_marker_fields(self):
|
|
104
|
-
fields = {f.name for f in dataclasses.fields(mapwright.Marker)}
|
|
105
|
-
assert {"name", "x", "y", "kind"} <= fields
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
class TestSerialisationContract:
|
|
109
|
-
"""The (de)serialisation surface is part of the public contract."""
|
|
110
|
-
|
|
111
|
-
def test_roundtrip_types_have_dict_methods(self):
|
|
112
|
-
for cls in (
|
|
113
|
-
mapwright.TerrainResult,
|
|
114
|
-
mapwright.Dungeon,
|
|
115
|
-
mapwright.Marker,
|
|
116
|
-
mapwright.TerrainCell,
|
|
117
|
-
mapwright.River,
|
|
118
|
-
mapwright.Rect,
|
|
119
|
-
mapwright.Road,
|
|
120
|
-
mapwright.Region,
|
|
121
|
-
mapwright.Settlement,
|
|
122
|
-
mapwright.Ward,
|
|
123
|
-
mapwright.Lot,
|
|
124
|
-
mapwright.Street,
|
|
125
|
-
mapwright.Wall,
|
|
126
|
-
mapwright.Landmark,
|
|
127
|
-
mapwright.SettlementConfig,
|
|
128
|
-
):
|
|
129
|
-
assert hasattr(cls, "to_dict") and callable(cls.to_dict)
|
|
130
|
-
assert hasattr(cls, "from_dict") and callable(cls.from_dict)
|
|
131
|
-
|
|
132
|
-
def test_top_level_types_have_json_methods(self):
|
|
133
|
-
for cls in (mapwright.TerrainResult, mapwright.Dungeon, mapwright.Marker,
|
|
134
|
-
mapwright.Settlement):
|
|
135
|
-
assert hasattr(cls, "to_json") and callable(cls.to_json)
|
|
136
|
-
assert hasattr(cls, "from_json") and callable(cls.from_json)
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
class TestConfigContract:
|
|
140
|
-
def test_spec_covers_every_field_exactly(self):
|
|
141
|
-
spec_names = {name for name, *_ in _SPEC}
|
|
142
|
-
field_names = {f.name for f in dataclasses.fields(WorldMapConfig)}
|
|
143
|
-
assert spec_names == field_names
|
|
144
|
-
|
|
145
|
-
def test_json_schema_shape(self):
|
|
146
|
-
schema = WorldMapConfig.json_schema()
|
|
147
|
-
assert schema["type"] == "object"
|
|
148
|
-
assert schema["additionalProperties"] is False
|
|
149
|
-
props = schema["properties"]
|
|
150
|
-
assert set(props) == {f.name for f in dataclasses.fields(WorldMapConfig)}
|
|
151
|
-
|
|
152
|
-
def test_json_schema_bounds_and_defaults_match(self):
|
|
153
|
-
schema = WorldMapConfig.json_schema()
|
|
154
|
-
defaults = WorldMapConfig()
|
|
155
|
-
for name, typ, lo, hi, _desc in _SPEC:
|
|
156
|
-
p = schema["properties"][name]
|
|
157
|
-
assert p["type"] == ("integer" if typ is int else "number")
|
|
158
|
-
assert p["minimum"] == lo and p["maximum"] == hi
|
|
159
|
-
assert p["default"] == getattr(defaults, name)
|
|
160
|
-
|
|
161
|
-
def test_schema_bounds_are_actually_enforced(self):
|
|
162
|
-
# A payload that violates the schema bounds is clamped into them.
|
|
163
|
-
for name, _typ, lo, hi, _desc in _SPEC:
|
|
164
|
-
below = WorldMapConfig.from_dict({name: lo - 100})
|
|
165
|
-
above = WorldMapConfig.from_dict({name: hi + 100})
|
|
166
|
-
assert getattr(below, name) >= lo
|
|
167
|
-
assert getattr(above, name) <= hi
|
|
168
|
-
|
|
169
|
-
def test_presets_are_valid_against_schema(self):
|
|
170
|
-
# Every preset must only use known keys within range (from_dict clamps,
|
|
171
|
-
# but presets should already be in-bounds by construction).
|
|
172
|
-
for name in WorldMapConfig.preset_names():
|
|
173
|
-
cfg = WorldMapConfig.preset(name)
|
|
174
|
-
for fname, _typ, lo, hi, _desc in _SPEC:
|
|
175
|
-
assert lo <= getattr(cfg, fname) <= hi
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|