mapFolding 0.3.12__py3-none-any.whl → 0.4.1__py3-none-any.whl

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.
Files changed (46) hide show
  1. mapFolding/__init__.py +40 -38
  2. mapFolding/basecamp.py +50 -50
  3. mapFolding/beDRY.py +336 -336
  4. mapFolding/oeis.py +262 -262
  5. mapFolding/reference/flattened.py +294 -293
  6. mapFolding/reference/hunterNumba.py +126 -126
  7. mapFolding/reference/irvineJavaPort.py +99 -99
  8. mapFolding/reference/jax.py +153 -153
  9. mapFolding/reference/lunnan.py +148 -148
  10. mapFolding/reference/lunnanNumpy.py +115 -115
  11. mapFolding/reference/lunnanWhile.py +114 -114
  12. mapFolding/reference/rotatedEntryPoint.py +183 -183
  13. mapFolding/reference/total_countPlus1vsPlusN.py +203 -203
  14. mapFolding/someAssemblyRequired/__init__.py +5 -1
  15. mapFolding/someAssemblyRequired/getLLVMforNoReason.py +12 -12
  16. mapFolding/someAssemblyRequired/makeJob.py +46 -52
  17. mapFolding/someAssemblyRequired/synthesizeModuleJAX.py +17 -17
  18. mapFolding/someAssemblyRequired/synthesizeNumba.py +343 -633
  19. mapFolding/someAssemblyRequired/synthesizeNumbaGeneralized.py +325 -0
  20. mapFolding/someAssemblyRequired/synthesizeNumbaJob.py +173 -0
  21. mapFolding/someAssemblyRequired/synthesizeNumbaModules.py +77 -0
  22. mapFolding/syntheticModules/__init__.py +0 -0
  23. mapFolding/syntheticModules/numba_countInitialize.py +4 -4
  24. mapFolding/syntheticModules/numba_countParallel.py +4 -4
  25. mapFolding/syntheticModules/numba_countSequential.py +4 -4
  26. mapFolding/syntheticModules/numba_doTheNeedful.py +7 -7
  27. mapFolding/theDao.py +165 -165
  28. mapFolding/theSSOT.py +177 -173
  29. mapFolding/theSSOTnumba.py +90 -74
  30. mapFolding-0.4.1.dist-info/METADATA +154 -0
  31. mapFolding-0.4.1.dist-info/RECORD +42 -0
  32. tests/conftest.py +253 -129
  33. tests/test_computations.py +79 -0
  34. tests/test_oeis.py +76 -85
  35. tests/test_other.py +136 -224
  36. tests/test_tasks.py +19 -23
  37. tests/test_types.py +2 -2
  38. mapFolding/someAssemblyRequired/synthesizeNumbaHardcoding.py +0 -188
  39. mapFolding-0.3.12.dist-info/METADATA +0 -155
  40. mapFolding-0.3.12.dist-info/RECORD +0 -40
  41. tests/conftest_tmpRegistry.py +0 -62
  42. tests/conftest_uniformTests.py +0 -53
  43. {mapFolding-0.3.12.dist-info → mapFolding-0.4.1.dist-info}/LICENSE +0 -0
  44. {mapFolding-0.3.12.dist-info → mapFolding-0.4.1.dist-info}/WHEEL +0 -0
  45. {mapFolding-0.3.12.dist-info → mapFolding-0.4.1.dist-info}/entry_points.txt +0 -0
  46. {mapFolding-0.3.12.dist-info → mapFolding-0.4.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,154 @@
1
+ Metadata-Version: 2.2
2
+ Name: mapFolding
3
+ Version: 0.4.1
4
+ Summary: Count distinct ways to fold a map (or a strip of stamps)
5
+ Author-email: Hunter Hogan <HunterHogan@pm.me>
6
+ License: CC-BY-NC-4.0
7
+ Project-URL: Donate, https://www.patreon.com/integrated
8
+ Project-URL: Homepage, https://github.com/hunterhogan/mapFolding
9
+ Project-URL: Repository, https://github.com/hunterhogan/mapFolding.git
10
+ Keywords: A001415,A001416,A001417,A001418,A195646,combinatorics,folding,map folding,OEIS,optimization,stamp folding
11
+ Classifier: Development Status :: 5 - Production/Stable
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: Education
14
+ Classifier: Intended Audience :: End Users/Desktop
15
+ Classifier: Intended Audience :: Other Audience
16
+ Classifier: Intended Audience :: Science/Research
17
+ Classifier: Natural Language :: English
18
+ Classifier: Operating System :: OS Independent
19
+ Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Programming Language :: Python :: 3.13
23
+ Classifier: Programming Language :: Python :: 3
24
+ Classifier: Programming Language :: Python
25
+ Classifier: Topic :: Scientific/Engineering :: Mathematics
26
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
27
+ Classifier: Typing :: Typed
28
+ Requires-Python: >=3.10
29
+ Description-Content-Type: text/markdown
30
+ License-File: LICENSE
31
+ Requires-Dist: numba
32
+ Requires-Dist: numpy
33
+ Requires-Dist: Z0Z_tools
34
+ Provides-Extra: testing
35
+ Requires-Dist: autoflake; extra == "testing"
36
+ Requires-Dist: more_itertools; extra == "testing"
37
+ Requires-Dist: pytest-cov; extra == "testing"
38
+ Requires-Dist: pytest-env; extra == "testing"
39
+ Requires-Dist: pytest-xdist; extra == "testing"
40
+ Requires-Dist: pytest; extra == "testing"
41
+ Requires-Dist: python_minifier; extra == "testing"
42
+ Requires-Dist: updateCitation; extra == "testing"
43
+
44
+ # mapFolding: Algorithms for enumerating distinct map/stamp folding patterns 🗺️
45
+
46
+ [![pip install mapFolding](https://img.shields.io/badge/pip%20install-mapFolding-gray.svg?colorB=3b434b)](https://pypi.org/project/mapFolding/)
47
+ [![Static Badge](https://img.shields.io/badge/stinkin'%20badges-don't%20need-b98e5e)](https://youtu.be/g6f_miE91mk&t=4)
48
+ [![Python Tests](https://github.com/hunterhogan/mapFolding/actions/workflows/pythonTests.yml/badge.svg)](https://github.com/hunterhogan/mapFolding/actions/workflows/pythonTests.yml)
49
+ ![Static Badge](https://img.shields.io/badge/issues-I%20have%20them-brightgreen)
50
+ [![License: CC-BY-NC-4.0](https://img.shields.io/badge/License-CC_BY--NC_4.0-3b434b)](https://creativecommons.org/licenses/by-nc/4.0/)
51
+ ![PyPI - Downloads](https://img.shields.io/pypi/dd/mapFolding)
52
+ ![GitHub repo size](https://img.shields.io/github/repo-size/hunterhogan/mapFolding)
53
+
54
+ ---
55
+
56
+ ## Quick start
57
+
58
+ ```sh
59
+ pip install mapFolding
60
+ ```
61
+
62
+ `OEIS_for_n` will run a computation from the command line.
63
+
64
+ ```cmd
65
+ (mapFolding) C:\apps\mapFolding> OEIS_for_n A001418 5
66
+ 186086600 distinct folding patterns.
67
+ Time elapsed: 1.605 seconds
68
+ ```
69
+
70
+ Use `mapFolding.oeisIDfor_n()` to compute a(n) for an OEIS ID.
71
+
72
+ ```python
73
+ from mapFolding import oeisIDfor_n
74
+ foldsTotal = oeisIDfor_n( 'A001418', 4 )
75
+ ```
76
+
77
+ ---
78
+
79
+ ## Features
80
+
81
+ ### 1. Simple, easy usage based on OEIS IDs
82
+
83
+ `mapFolding` directly implements some IDs from [_The On-Line Encyclopedia of Integer Sequences_](https://oeis.org/) ([BibTex](https://github.com/hunterhogan/mapFolding/blob/main/citations/oeis.bibtex) citation).
84
+
85
+ Use `getOEISids` to get the most up-to-date list of available OEIS IDs.
86
+
87
+ ```cmd
88
+ (mapFolding) C:\apps\mapFolding> getOEISids
89
+
90
+ Available OEIS sequences:
91
+ A001415: Number of ways of folding a 2 X n strip of stamps.
92
+ A001416: Number of ways of folding a 3 X n strip of stamps.
93
+ A001417: Number of ways of folding a 2 X 2 X ... X 2 n-dimensional map.
94
+ A001418: Number of ways of folding an n X n sheet of stamps.
95
+ A195646: Number of ways of folding a 3 X 3 X ... X 3 n-dimensional map.
96
+ ```
97
+
98
+ ### 2. **Algorithm Zoo** 🦒
99
+
100
+ - **Lunnon’s 1971 Algorithm**: A painstakingly debugged version of [the original typo-riddled code](https://github.com/hunterhogan/mapFolding/blob/mapFolding/reference/foldings.txt)
101
+ - The /reference directory.
102
+ - **Numba-JIT Accelerated**: Up to 1000× faster than pure Python ([benchmarks](https://github.com/hunterhogan/mapFolding/blob/mapFolding/notes/Speed%20highlights.md))
103
+
104
+ ### 3. **For Researchers** 🔬
105
+
106
+ - Change multiple minute settings, such as the bit width of the data types.
107
+ - Transform the algorithm using AST
108
+ - Create hyper-optimized modules to compute a specific map.
109
+
110
+ ### 4. **Customizing your algorithm**
111
+
112
+ - mapFolding\someAssemblyRequired\synthesizeNumbaJob.py (and/or synthesizeNumba____.py, as applicable)
113
+ - Synthesize a Numba-optimized module for a specific mapShape
114
+ - Synthesize _from_ a module in mapFolding\syntheticModules or from any source you select
115
+ - Use the existing transformation options
116
+ - Or create new ways of transforming the algorithm from its source to a specific job
117
+ - mapFolding\someAssemblyRequired\makeJob.py
118
+ - Initialize data for a specific mapShape
119
+ - mapFolding\someAssemblyRequired\synthesizeNumbaModules.py (and/or synthesizeNumba____.py, as applicable)
120
+ - Synthesize one or more Numba-optimized modules for parallel or sequential computation
121
+ - Overwrite the modules in mapFolding\syntheticModules or save the module(s) to a custom path
122
+ - Synthesize _from_ the algorithm(s) in mapFolding\theDao.py or from any source you select
123
+ - Use the existing transformation options
124
+ - Or create new ways of transforming the algorithm from its source to a new module
125
+ - Use your new module in synthesizeNumbaJob.py, above, as the source to create a mapShape-specific job module
126
+ - mapFolding\theDao.py
127
+ - Modify the algorithms for initializing values, parallel computation, and/or sequential computation
128
+ - Use the modified algorithm(s) in synthesizeNumbaModules.py, above, to create Numba-optimized version(s)
129
+ - Then use a Numba-optimized version in synthesizeNumbaJob.py, above, to create a hyper-optimized version for a specific mapShape
130
+ - mapFolding\theSSOT.py (and/or theSSOTnumba.py and/ or theSSOT____.py, if they exist)
131
+ - Modify broad settings or find functions to modify broad settings, such as data structures and their data types
132
+ - Create new settings or groups of settings
133
+ - mapFolding\beDRY.py
134
+ - Functions to handle common tasks, such as parsing parameters or creating the `connectionGraph` for a mapShape (a Cartesian product decomposition)
135
+ - mapFolding\someAssemblyRequired
136
+ - Create new transformations to optimize the algorithm, such as for JAX, CuPy, or CUDA
137
+ - (mapFolding\reference\jax.py has a once-functional JAX implementation, and synthesizeModuleJAX.py might be a useful starting point)
138
+
139
+ ## Map-folding Video
140
+
141
+ ~~This caused my neurosis:~~ I enjoyed the following video, which is what introduced me to map folding.
142
+
143
+ "How Many Ways Can You Fold a Map?" by Physics for the Birds, 2024 November 13 ([BibTex](https://github.com/hunterhogan/mapFolding/blob/main/citations/Physics_for_the_Birds.bibtex) citation)
144
+
145
+ [![How Many Ways Can You Fold a Map?](https://i.ytimg.com/vi/sfH9uIY3ln4/hq720.jpg)](https://www.youtube.com/watch?v=sfH9uIY3ln4)
146
+
147
+ ---
148
+
149
+ ## My recovery
150
+
151
+ [![Static Badge](https://img.shields.io/badge/2011_August-Homeless_since-blue?style=flat)](https://HunterThinks.com/support)
152
+ [![YouTube Channel Subscribers](https://img.shields.io/youtube/channel/subscribers/UC3Gx7kz61009NbhpRtPP7tw)](https://www.youtube.com/@HunterHogan)
153
+
154
+ [![CC-BY-NC-4.0](https://github.com/hunterhogan/mapFolding/blob/main/CC-BY-NC-4.0.svg)](https://creativecommons.org/licenses/by-nc/4.0/)
@@ -0,0 +1,42 @@
1
+ mapFolding/__init__.py,sha256=_YjoypHXmWLmEWwhFVgKO83Uf28ksesT9F73oJoAIPE,1323
2
+ mapFolding/basecamp.py,sha256=v0VCF_Zgm_XBHcz4bqblsxfHwAxZKgenW6um77quWLk,3751
3
+ mapFolding/beDRY.py,sha256=XVtLraG9VnC4yG2HkaFwZRh2td4ZHMjTQvnbcD_W130,17133
4
+ mapFolding/oeis.py,sha256=3hv71o8bhckjY0nsSY5JTJ2LrpJcuhZ9j3mP6LWLIQc,11124
5
+ mapFolding/theDao.py,sha256=SmyTbP1iwRAnpuq2ngdJKooXUA1_PR0VRHQ4fcJskMY,12713
6
+ mapFolding/theSSOT.py,sha256=QrEMPREjEbt1H8HcrM2Nm_hv7JsFWRG3lHdUU0Jrv-w,10238
7
+ mapFolding/theSSOTnumba.py,sha256=zGq2zlZZeuxiNSO2Fs_AqV6UhybJAJuDw-2lMVvDS2w,5133
8
+ mapFolding/reference/flattened.py,sha256=S6D9wiFTlbeoetEqaMLOcA-R22BHOzjqPRujffNxxUM,14875
9
+ mapFolding/reference/hunterNumba.py,sha256=jDS0ORHkIhcJ1rzA5hT49sZHKf3rgJOoGesUCcbKFFY,6054
10
+ mapFolding/reference/irvineJavaPort.py,sha256=7GvBU0tnS6wpFgkYad3465do9jBQW-2bYvbCYyABPHM,3341
11
+ mapFolding/reference/jax.py,sha256=7ji9YWia6Kof0cjcNdiS1GG1rMbC5SBjcyVr_07AeUk,13845
12
+ mapFolding/reference/lunnan.py,sha256=iAbJELfW6RKNMdPcBY9b6rGQ-z1zoRf-1XCurCRMOo8,3951
13
+ mapFolding/reference/lunnanNumpy.py,sha256=rwVP3WIDXimpAuaxhRIuBYU56nVDTKlfGiclw_FkgUU,3765
14
+ mapFolding/reference/lunnanWhile.py,sha256=uRrMT23jTJvoQDlD_FzeIQe_pfMXJG6_bRvs7uhC8z0,3271
15
+ mapFolding/reference/rotatedEntryPoint.py,sha256=USZY3n3zwhSE68ATscUuN66t1qShuEbMI790Gz9JFTw,9352
16
+ mapFolding/reference/total_countPlus1vsPlusN.py,sha256=wpgay-uqPOBd64Z4Pg6tg40j7-4pzWHGMM6v0bnmjhE,6288
17
+ mapFolding/someAssemblyRequired/__init__.py,sha256=wtec_hIz-AKz0_hGdXsWnCKTcCxdMV9-WK6SiIriAeU,396
18
+ mapFolding/someAssemblyRequired/getLLVMforNoReason.py,sha256=nX8tghZClYt7zJd6RpZBXhE_h-CGRHOS17biqiEdf-o,855
19
+ mapFolding/someAssemblyRequired/makeJob.py,sha256=c9sTRUK90snTCcXCvs86VKBH6z_nt3OVFjNs_WgCoIg,2422
20
+ mapFolding/someAssemblyRequired/synthesizeModuleJAX.py,sha256=jatvtYhK5ZJK-YmCKATt7w3icFXXO79cZDAYVrU9bgA,1258
21
+ mapFolding/someAssemblyRequired/synthesizeNumba.py,sha256=mPCjp4N-dOJRC4TvZGkqAqFKDWEPhWH9v0Cq5AWHlBA,17279
22
+ mapFolding/someAssemblyRequired/synthesizeNumbaGeneralized.py,sha256=k8IaCT74ZPhHyra0MbCRdt_5k0Ov3vJgXlN5tbLVnf4,13998
23
+ mapFolding/someAssemblyRequired/synthesizeNumbaJob.py,sha256=2sKZgc5kyyz2KaoApcazj_37UgBqAkxORFeROWWU5tk,9038
24
+ mapFolding/someAssemblyRequired/synthesizeNumbaModules.py,sha256=_iRXjMASB_BnYJeH8Rt7FlC-GE7lkZ1Hy292XTaUCu4,3785
25
+ mapFolding/syntheticModules/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
26
+ mapFolding/syntheticModules/numba_countInitialize.py,sha256=geHketfekZTgu5gbc8E3SShPmbW3gDybg5PCBpXdsa8,4274
27
+ mapFolding/syntheticModules/numba_countParallel.py,sha256=kOI5PU90AExPvlWwU0BHVVFjlHlmUFp1KdtlmthQ71E,5517
28
+ mapFolding/syntheticModules/numba_countSequential.py,sha256=zFFRv9oLtOih9TpbtARpVAPt-NfZxh0ygXuj-wfPjUg,3732
29
+ mapFolding/syntheticModules/numba_doTheNeedful.py,sha256=6WuXKDMVa_C56dLlmXNvFl04MlU8-WVasqbAaxsgI-o,1368
30
+ tests/__init__.py,sha256=eg9smg-6VblOr0kisM40CpGnuDtU2JgEEWGDTFVOlW8,57
31
+ tests/conftest.py,sha256=7Ims3QcOzqBXu_k0kX9bt6PieC-OoIpc7OGxzdT2ELc,11826
32
+ tests/test_computations.py,sha256=qBha4IggMfr6ZH06W3M66enTA6PWsx8vkDp5eqYFM9M,4765
33
+ tests/test_oeis.py,sha256=31kdO1vnu2Lon43vM-YJVS4g40Ic03DWNER-cJcpxX4,4916
34
+ tests/test_other.py,sha256=u0vINT5EyVsXTNTR2DZIMpWCg4FH471jjHLRzC2JX7U,8351
35
+ tests/test_tasks.py,sha256=iq6_dh43JQkC2vAWXua0Xe915BKFGbvRJAkmbco854A,2389
36
+ tests/test_types.py,sha256=58tmPG9WOeGGAQbdQK_h_7t4SnENnZugH4WXlI8-L-M,171
37
+ mapFolding-0.4.1.dist-info/LICENSE,sha256=NxH5Y8BdC-gNU-WSMwim3uMbID2iNDXJz7fHtuTdXhk,19346
38
+ mapFolding-0.4.1.dist-info/METADATA,sha256=iJiWfEzXVheLtyLLWT2BNis5xsisnhllS17hnPwiRws,7633
39
+ mapFolding-0.4.1.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
40
+ mapFolding-0.4.1.dist-info/entry_points.txt,sha256=F3OUeZR1XDTpoH7k3wXuRb3KF_kXTTeYhu5AGK1SiOQ,146
41
+ mapFolding-0.4.1.dist-info/top_level.txt,sha256=1gP2vFaqPwHujGwb3UjtMlLEGN-943VSYFR7V4gDqW8,17
42
+ mapFolding-0.4.1.dist-info/RECORD,,
tests/conftest.py CHANGED
@@ -2,182 +2,306 @@
2
2
 
3
3
  # TODO learn how to run tests and coverage analysis without `env = ["NUMBA_DISABLE_JIT=1"]`
4
4
 
5
- from tests.conftest_tmpRegistry import (
6
- pathCacheTesting,
7
- pathDataSamples,
8
- pathFilenameFoldsTotalTesting,
9
- pathTempTesting,
10
- setupTeardownTestData,
11
- )
12
- from tests.conftest_uniformTests import (
13
- uniformTestMessage,
14
- standardizedEqualTo,
15
- standardizedSystemExit,
16
- )
17
5
  from mapFolding import *
18
- from mapFolding import basecamp
19
- from mapFolding import getAlgorithmCallable, getDispatcherCallable
6
+ from mapFolding import basecamp, getAlgorithmDispatcher, getDispatcherCallable
20
7
  from mapFolding.beDRY import *
21
- from mapFolding.oeis import _getFilenameOEISbFile, _getOEISidValues, _getOEISidInformation
8
+ from mapFolding.someAssemblyRequired import *
9
+ from mapFolding.oeis import _getFilenameOEISbFile, _getOEISidInformation, _getOEISidValues
22
10
  from mapFolding.oeis import *
11
+ from types import ModuleType
12
+ from typing import Any, Callable, ContextManager, Dict, Generator, List, Literal, NoReturn, Optional, Sequence, Set, Tuple, Type, Union
23
13
  from Z0Z_tools.pytestForYourUse import PytestFor_defineConcurrencyLimit, PytestFor_intInnit, PytestFor_oopsieKwargsie
24
- from typing import Any, Callable, ContextManager, Dict, Generator, List, Optional, Sequence, Set, Tuple, Type, Union
25
14
  import pathlib
26
15
  import pytest
27
16
  import random
17
+ import shutil
28
18
  import unittest.mock
19
+ import uuid
20
+
21
+ # SSOT for test data paths and filenames
22
+ pathDataSamples = pathlib.Path("tests/dataSamples")
23
+ # NOTE `tmp` is not a diminutive form of temporary: it signals a technical term. And "temp" is strongly disfavored.
24
+ pathTmpRoot = pathDataSamples / "tmp"
25
+
26
+ # The registrar maintains the register of temp files
27
+ registerOfTemporaryFilesystemObjects: Set[pathlib.Path] = set()
28
+
29
+ def registrarRecordsTmpObject(path: pathlib.Path) -> None:
30
+ """The registrar adds a tmp file to the register."""
31
+ registerOfTemporaryFilesystemObjects.add(path)
32
+
33
+ def registrarDeletesTmpObjects() -> None:
34
+ """The registrar cleans up tmp files in the register."""
35
+ for pathTmp in sorted(registerOfTemporaryFilesystemObjects, reverse=True):
36
+ try:
37
+ if pathTmp.is_file():
38
+ pathTmp.unlink(missing_ok=True)
39
+ elif pathTmp.is_dir():
40
+ shutil.rmtree(pathTmp, ignore_errors=True)
41
+ except Exception as ERRORmessage:
42
+ print(f"Warning: Failed to clean up {pathTmp}: {ERRORmessage}")
43
+ registerOfTemporaryFilesystemObjects.clear()
44
+
45
+ @pytest.fixture(scope="session", autouse=True)
46
+ def setupTeardownTmpObjects() -> Generator[None, None, None]:
47
+ """Auto-fixture to setup test data directories and cleanup after."""
48
+ pathDataSamples.mkdir(exist_ok=True)
49
+ pathTmpRoot.mkdir(exist_ok=True)
50
+ yield
51
+ registrarDeletesTmpObjects()
52
+
53
+ @pytest.fixture
54
+ def pathTmpTesting(request: pytest.FixtureRequest) -> pathlib.Path:
55
+ # "Z0Z_" ensures the directory name does not start with a number, which would make it an invalid Python identifier
56
+ pathTmp = pathTmpRoot / ("Z0Z_" + str(uuid.uuid4().hex))
57
+ pathTmp.mkdir(parents=True, exist_ok=False)
58
+
59
+ registrarRecordsTmpObject(pathTmp)
60
+ return pathTmp
61
+
62
+ @pytest.fixture
63
+ def pathFilenameTmpTesting(request: pytest.FixtureRequest) -> pathlib.Path:
64
+ try:
65
+ extension = request.param
66
+ except AttributeError:
67
+ extension = ".txt"
68
+
69
+ # "Z0Z_" ensures the name does not start with a number, which would make it an invalid Python identifier
70
+ uuidHex = uuid.uuid4().hex
71
+ subpath = "Z0Z_" + uuidHex[0:-8]
72
+ filenameStem = "Z0Z_" + uuidHex[-8:None]
73
+
74
+ pathFilenameTmp = pathlib.Path(pathTmpRoot, subpath, filenameStem + extension)
75
+ pathFilenameTmp.parent.mkdir(parents=True, exist_ok=False)
76
+
77
+ registrarRecordsTmpObject(pathFilenameTmp)
78
+ return pathFilenameTmp
79
+
80
+ @pytest.fixture
81
+ def pathCacheTesting(pathTmpTesting: pathlib.Path) -> Generator[pathlib.Path, Any, None]:
82
+ """Temporarily replace the OEIS cache directory with a test directory."""
83
+ from mapFolding import oeis as there_must_be_a_better_way
84
+ pathCacheOriginal = there_must_be_a_better_way._pathCache
85
+ there_must_be_a_better_way._pathCache = pathTmpTesting
86
+ yield pathTmpTesting
87
+ there_must_be_a_better_way._pathCache = pathCacheOriginal
88
+
89
+ @pytest.fixture
90
+ def pathFilenameFoldsTotalTesting(pathTmpTesting: pathlib.Path) -> pathlib.Path:
91
+ return pathTmpTesting.joinpath("foldsTotalTest.txt")
29
92
 
30
93
  def makeDictionaryFoldsTotalKnown() -> Dict[Tuple[int,...], int]:
31
- """Returns a dictionary mapping dimension tuples to their known folding totals."""
32
- dictionaryMapDimensionsToFoldsTotalKnown: Dict[Tuple[int, ...], int] = {}
33
-
34
- for settings in settingsOEIS.values():
35
- sequence = settings['valuesKnown']
36
-
37
- for n, foldingsTotal in sequence.items():
38
- dimensions = settings['getMapShape'](n)
39
- dimensions.sort()
40
- dictionaryMapDimensionsToFoldsTotalKnown[tuple(dimensions)] = foldingsTotal
41
-
42
- # Are we in a place that has jobs?
43
- pathJobDEFAULT = getPathJobRootDEFAULT()
44
- if pathJobDEFAULT.exists():
45
- # Are there foldsTotal files?
46
- for pathFilenameFoldsTotal in pathJobDEFAULT.rglob('*.foldsTotal'):
47
- if pathFilenameFoldsTotal.is_file():
48
- try:
49
- listDimensions = eval(pathFilenameFoldsTotal.stem)
50
- except Exception:
51
- continue
52
- # Are the dimensions in the dictionary?
53
- if isinstance(listDimensions, list) and all(isinstance(dimension, int) for dimension in listDimensions):
54
- listDimensions.sort()
55
- if tuple(listDimensions) in dictionaryMapDimensionsToFoldsTotalKnown:
56
- continue
57
- # Are the contents a reasonably large integer?
58
- try:
59
- foldsTotal = pathFilenameFoldsTotal.read_text()
60
- except Exception:
61
- continue
62
- # Why did I sincerely believe this would only be three lines of code?
63
- if foldsTotal.isdigit():
64
- foldsTotalInteger = int(foldsTotal)
65
- if foldsTotalInteger > 85109616 * 10**3:
66
- # You made it this far, so fuck it: put it in the dictionary
67
- dictionaryMapDimensionsToFoldsTotalKnown[tuple(listDimensions)] = foldsTotalInteger
68
- dictionaryMapDimensionsToFoldsTotalKnown[tuple(listDimensions)] = foldsTotalInteger
69
- # The sunk-costs fallacy claims another victim!
70
-
71
- return dictionaryMapDimensionsToFoldsTotalKnown
94
+ """Returns a dictionary mapping dimension tuples to their known folding totals."""
95
+ dictionaryMapDimensionsToFoldsTotalKnown: Dict[Tuple[int, ...], int] = {}
96
+
97
+ for settings in settingsOEIS.values():
98
+ sequence = settings['valuesKnown']
99
+
100
+ for n, foldingsTotal in sequence.items():
101
+ dimensions = settings['getMapShape'](n)
102
+ dimensions.sort()
103
+ dictionaryMapDimensionsToFoldsTotalKnown[tuple(dimensions)] = foldingsTotal
104
+
105
+ # Are we in a place that has jobs?
106
+ pathJobDEFAULT = getPathJobRootDEFAULT()
107
+ if pathJobDEFAULT.exists():
108
+ # Are there foldsTotal files?
109
+ for pathFilenameFoldsTotal in pathJobDEFAULT.rglob('*.foldsTotal'):
110
+ if pathFilenameFoldsTotal.is_file():
111
+ try:
112
+ listDimensions = eval(pathFilenameFoldsTotal.stem)
113
+ except Exception:
114
+ continue
115
+ # Are the dimensions in the dictionary?
116
+ if isinstance(listDimensions, list) and all(isinstance(dimension, int) for dimension in listDimensions):
117
+ listDimensions.sort()
118
+ if tuple(listDimensions) in dictionaryMapDimensionsToFoldsTotalKnown:
119
+ continue
120
+ # Are the contents a reasonably large integer?
121
+ try:
122
+ foldsTotal = pathFilenameFoldsTotal.read_text()
123
+ except Exception:
124
+ continue
125
+ # Why did I sincerely believe this would only be three lines of code?
126
+ if foldsTotal.isdigit():
127
+ foldsTotalInteger = int(foldsTotal)
128
+ if foldsTotalInteger > 85109616 * 10**3:
129
+ # You made it this far, so fuck it: put it in the dictionary
130
+ dictionaryMapDimensionsToFoldsTotalKnown[tuple(listDimensions)] = foldsTotalInteger
131
+ dictionaryMapDimensionsToFoldsTotalKnown[tuple(listDimensions)] = foldsTotalInteger
132
+ # The sunk-costs fallacy claims another victim!
133
+
134
+ return dictionaryMapDimensionsToFoldsTotalKnown
72
135
 
73
136
  """
74
137
  Section: Fixtures"""
75
138
 
76
139
  @pytest.fixture(autouse=True)
77
140
  def setupWarningsAsErrors() -> Generator[None, Any, None]:
78
- """Convert all warnings to errors for all tests."""
79
- import warnings
80
- warnings.filterwarnings("error")
81
- yield
82
- warnings.resetwarnings()
141
+ """Convert all warnings to errors for all tests."""
142
+ import warnings
143
+ warnings.filterwarnings("error")
144
+ yield
145
+ warnings.resetwarnings()
83
146
 
84
147
  @pytest.fixture
85
148
  def foldsTotalKnown() -> Dict[Tuple[int,...], int]:
86
- """Returns a dictionary mapping dimension tuples to their known folding totals.
87
- NOTE I am not convinced this is the best way to do this.
88
- Advantage: I call `makeDictionaryFoldsTotalKnown()` from modules other than test modules.
89
- Preference: I _think_ I would prefer a SSOT function available to any module
90
- similar to `foldsTotalKnown = getFoldsTotalKnown(listDimensions)`."""
91
- return makeDictionaryFoldsTotalKnown()
149
+ """Returns a dictionary mapping dimension tuples to their known folding totals.
150
+ NOTE I am not convinced this is the best way to do this.
151
+ Advantage: I call `makeDictionaryFoldsTotalKnown()` from modules other than test modules.
152
+ Preference: I _think_ I would prefer a SSOT function available to any module
153
+ similar to `foldsTotalKnown = getFoldsTotalKnown(listDimensions)`."""
154
+ return makeDictionaryFoldsTotalKnown()
92
155
 
93
156
  @pytest.fixture
94
157
  def listDimensionsTestCountFolds(oeisID: str) -> List[int]:
95
- """For each `oeisID` from the `pytest.fixture`, returns `listDimensions` from `valuesTestValidation`
96
- if `validateListDimensions` approves. Each `listDimensions` is suitable for testing counts."""
97
- while True:
98
- n = random.choice(settingsOEIS[oeisID]['valuesTestValidation'])
99
- if n < 2:
100
- continue
101
- listDimensionsCandidate = settingsOEIS[oeisID]['getMapShape'](n)
102
-
103
- try:
104
- return validateListDimensions(listDimensionsCandidate)
105
- except (ValueError, NotImplementedError):
106
- pass
158
+ """For each `oeisID` from the `pytest.fixture`, returns `listDimensions` from `valuesTestValidation`
159
+ if `validateListDimensions` approves. Each `listDimensions` is suitable for testing counts."""
160
+ while True:
161
+ n = random.choice(settingsOEIS[oeisID]['valuesTestValidation'])
162
+ if n < 2:
163
+ continue
164
+ listDimensionsCandidate = settingsOEIS[oeisID]['getMapShape'](n)
165
+
166
+ try:
167
+ return validateListDimensions(listDimensionsCandidate)
168
+ except (ValueError, NotImplementedError):
169
+ pass
107
170
 
108
171
  @pytest.fixture
109
172
  def listDimensionsTestFunctionality(oeisID_1random: str) -> List[int]:
110
- """To test functionality, get one `listDimensions` from `valuesTestValidation` if
111
- `validateListDimensions` approves. The algorithm can count the folds of the returned
112
- `listDimensions` in a short enough time suitable for testing."""
113
- while True:
114
- n = random.choice(settingsOEIS[oeisID_1random]['valuesTestValidation'])
115
- if n < 2:
116
- continue
117
- listDimensionsCandidate = settingsOEIS[oeisID_1random]['getMapShape'](n)
118
-
119
- try:
120
- return validateListDimensions(listDimensionsCandidate)
121
- except (ValueError, NotImplementedError):
122
- pass
173
+ """To test functionality, get one `listDimensions` from `valuesTestValidation` if
174
+ `validateListDimensions` approves. The algorithm can count the folds of the returned
175
+ `listDimensions` in a short enough time suitable for testing."""
176
+ while True:
177
+ n = random.choice(settingsOEIS[oeisID_1random]['valuesTestValidation'])
178
+ if n < 2:
179
+ continue
180
+ listDimensionsCandidate = settingsOEIS[oeisID_1random]['getMapShape'](n)
181
+
182
+ try:
183
+ return validateListDimensions(listDimensionsCandidate)
184
+ except (ValueError, NotImplementedError):
185
+ pass
123
186
 
124
187
  @pytest.fixture
125
188
  def listDimensionsTestParallelization(oeisID: str) -> List[int]:
126
- """For each `oeisID` from the `pytest.fixture`, returns `listDimensions` from `valuesTestParallelization`"""
127
- n = random.choice(settingsOEIS[oeisID]['valuesTestParallelization'])
128
- return settingsOEIS[oeisID]['getMapShape'](n)
189
+ """For each `oeisID` from the `pytest.fixture`, returns `listDimensions` from `valuesTestParallelization`"""
190
+ n = random.choice(settingsOEIS[oeisID]['valuesTestParallelization'])
191
+ return settingsOEIS[oeisID]['getMapShape'](n)
129
192
 
130
193
  @pytest.fixture
131
194
  def mockBenchmarkTimer() -> Generator[unittest.mock.MagicMock | unittest.mock.AsyncMock, Any, None]:
132
- """Mock time.perf_counter_ns for consistent benchmark timing."""
133
- with unittest.mock.patch('time.perf_counter_ns') as mockTimer:
134
- mockTimer.side_effect = [0, 1e9] # Start and end times for 1 second
135
- yield mockTimer
195
+ """Mock time.perf_counter_ns for consistent benchmark timing."""
196
+ with unittest.mock.patch('time.perf_counter_ns') as mockTimer:
197
+ mockTimer.side_effect = [0, 1e9] # Start and end times for 1 second
198
+ yield mockTimer
136
199
 
137
200
  @pytest.fixture
138
201
  def mockFoldingFunction() -> Callable[..., Callable[..., None]]:
139
- """Creates a mock function that simulates _countFolds behavior."""
140
- def make_mock(foldsValue: int, listDimensions: List[int]) -> Callable[..., None]:
141
- mock_array = makeDataContainer(2)
142
- mock_array[0] = foldsValue
143
- mock_array[-1] = getLeavesTotal(listDimensions)
202
+ """Creates a mock function that simulates _countFolds behavior."""
203
+ def make_mock(foldsValue: int, listDimensions: List[int]) -> Callable[..., None]:
204
+ mock_array = makeDataContainer(2)
205
+ mock_array[0] = foldsValue
206
+ mock_array[-1] = getLeavesTotal(listDimensions)
144
207
 
145
- def mock_countFolds(**keywordArguments: Any) -> None:
146
- keywordArguments['foldGroups'][:] = mock_array
147
- return None
208
+ def mock_countFolds(**keywordArguments: Any) -> None:
209
+ keywordArguments['foldGroups'][:] = mock_array
210
+ return None
148
211
 
149
- return mock_countFolds
150
- return make_mock
212
+ return mock_countFolds
213
+ return make_mock
151
214
 
152
215
  @pytest.fixture
153
216
  def mockDispatcher() -> Callable[[Any], ContextManager[Any]]:
154
- """Context manager for mocking dispatcher callable."""
155
- def wrapper(mockFunction: Any) -> ContextManager[Any]:
156
- dispatcherCallable = getDispatcherCallable()
157
- return unittest.mock.patch(
158
- f"{dispatcherCallable.__module__}.{dispatcherCallable.__name__}",
159
- side_effect=mockFunction
160
- )
161
- return wrapper
217
+ """Context manager for mocking dispatcher callable."""
218
+ def wrapper(mockFunction: Any) -> ContextManager[Any]:
219
+ dispatcherCallable = getDispatcherCallable()
220
+ return unittest.mock.patch(
221
+ f"{dispatcherCallable.__module__}.{dispatcherCallable.__name__}",
222
+ side_effect=mockFunction
223
+ )
224
+ return wrapper
162
225
 
163
226
  @pytest.fixture(params=oeisIDsImplemented)
164
227
  def oeisID(request: pytest.FixtureRequest) -> Any:
165
- return request.param
228
+ return request.param
166
229
 
167
230
  @pytest.fixture
168
231
  def oeisID_1random() -> str:
169
- """Return one random valid OEIS ID."""
170
- return random.choice(oeisIDsImplemented)
232
+ """Return one random valid OEIS ID."""
233
+ return random.choice(oeisIDsImplemented)
234
+
235
+ @pytest.fixture
236
+ def useThisDispatcher():
237
+ """A fixture providing a context manager for temporarily replacing the dispatcher.
238
+
239
+ Returns
240
+ A context manager for patching the dispatcher
241
+ """
242
+ dispatcherOriginal = basecamp.getDispatcherCallable
243
+
244
+ def patchDispatcher(callableTarget: Callable) -> None:
245
+ def callableParameterized(*arguments: Any, **keywordArguments: Any) -> Callable:
246
+ return callableTarget
247
+ basecamp.getDispatcherCallable = callableParameterized
248
+
249
+ yield patchDispatcher
250
+ basecamp.getDispatcherCallable = dispatcherOriginal
171
251
 
172
252
  @pytest.fixture
173
- def useAlgorithmDirectly() -> Generator[None, Any, None]:
174
- """Temporarily patches getDispatcherCallable to return the algorithm source directly."""
175
- original_dispatcher = basecamp.getDispatcherCallable
253
+ def useAlgorithmSourceDispatcher(useThisDispatcher: Callable) -> Generator[None, None, None]:
254
+ """Temporarily patches getDispatcherCallable to return the algorithm dispatcher."""
255
+ useThisDispatcher(getAlgorithmDispatcher())
256
+ yield
257
+
258
+ def uniformTestMessage(expected: Any, actual: Any, functionName: str, *arguments: Any) -> str:
259
+ """Format assertion message for any test comparison."""
260
+ return (f"\nTesting: `{functionName}({', '.join(str(parameter) for parameter in arguments)})`\n"
261
+ f"Expected: {expected}\n"
262
+ f"Got: {actual}")
263
+
264
+ def standardizedEqualTo(expected: Any, functionTarget: Callable, *arguments: Any) -> None:
265
+ """Template for tests expecting an error."""
266
+ if type(expected) is Type[Exception]:
267
+ messageExpected = expected.__name__
268
+ else:
269
+ messageExpected = expected
270
+
271
+ try:
272
+ messageActual = actual = functionTarget(*arguments)
273
+ except Exception as actualError:
274
+ messageActual = type(actualError).__name__
275
+ actual = type(actualError)
276
+
277
+ assert actual == expected, uniformTestMessage(messageExpected, messageActual, functionTarget.__name__, *arguments)
278
+
279
+ def standardizedSystemExit(expected: Union[str, int, Sequence[int]], functionTarget: Callable, *arguments: Any) -> None:
280
+ """Template for tests expecting SystemExit.
176
281
 
177
- # Patch the function at module level
178
- basecamp.getDispatcherCallable = getAlgorithmCallable
282
+ Parameters
283
+ expected: Exit code expectation:
284
+ - "error": any non-zero exit code
285
+ - "nonError": specifically zero exit code
286
+ - int: exact exit code match
287
+ - Sequence[int]: exit code must be one of these values
288
+ functionTarget: The function to test
289
+ arguments: Arguments to pass to the function
290
+ """
291
+ with pytest.raises(SystemExit) as exitInfo:
292
+ functionTarget(*arguments)
179
293
 
180
- yield
294
+ exitCode = exitInfo.value.code
181
295
 
182
- # Restore original function
183
- basecamp.getDispatcherCallable = original_dispatcher
296
+ if expected == "error":
297
+ assert exitCode != 0, \
298
+ f"Expected error exit (non-zero) but got code {exitCode}"
299
+ elif expected == "nonError":
300
+ assert exitCode == 0, \
301
+ f"Expected non-error exit (0) but got code {exitCode}"
302
+ elif isinstance(expected, (list, tuple)):
303
+ assert exitCode in expected, \
304
+ f"Expected exit code to be one of {expected} but got {exitCode}"
305
+ else:
306
+ assert exitCode == expected, \
307
+ f"Expected exit code {expected} but got {exitCode}"