mapFolding 0.2.0__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.
mapFolding/theSSOT.py ADDED
@@ -0,0 +1,62 @@
1
+ from typing import Any, Tuple, TypedDict
2
+ import enum
3
+ import numpy
4
+ import numpy.typing
5
+ import pathlib
6
+ import sys
7
+
8
+ try:
9
+ _pathModule = pathlib.Path(__file__).parent
10
+ except NameError:
11
+ _pathModule = pathlib.Path.cwd()
12
+
13
+ pathJobDEFAULT = _pathModule / "jobs"
14
+ """filenameFoldsTotal = str(sorted(mapShape)).replace(' ', '') + '.foldsTotal'"""
15
+ if 'google.colab' in sys.modules:
16
+ pathJobDEFAULT = pathlib.Path("/content/drive/MyDrive") / "jobs"
17
+
18
+ @enum.verify(enum.CONTINUOUS, enum.UNIQUE) if sys.version_info >= (3, 11) else lambda x: x
19
+ class EnumIndices(enum.IntEnum):
20
+ """Base class for index enums."""
21
+ @staticmethod
22
+ def _generate_next_value_(name, start, count, last_values):
23
+ """0-indexed."""
24
+ return count
25
+
26
+ def __index__(self) -> int:
27
+ """Make the enum work with array indexing."""
28
+ return self.value
29
+
30
+ class indexMy(EnumIndices):
31
+ """Indices for dynamic values."""
32
+ dimension1ndex = enum.auto()
33
+ dimensionsUnconstrained = enum.auto()
34
+ gap1ndex = enum.auto()
35
+ gap1ndexCeiling = enum.auto()
36
+ indexLeaf = enum.auto()
37
+ indexMiniGap = enum.auto()
38
+ leaf1ndex = enum.auto()
39
+ leafConnectee = enum.auto()
40
+ taskIndex = enum.auto()
41
+
42
+ class indexThe(EnumIndices):
43
+ """Indices for static values."""
44
+ dimensionsTotal = enum.auto()
45
+ leavesTotal = enum.auto()
46
+ taskDivisions = enum.auto()
47
+
48
+ class indexTrack(EnumIndices):
49
+ """Indices for state tracking array."""
50
+ leafAbove = enum.auto()
51
+ leafBelow = enum.auto()
52
+ countDimensionsGapped = enum.auto()
53
+ gapRangeStart = enum.auto()
54
+
55
+ class computationState(TypedDict):
56
+ connectionGraph: numpy.typing.NDArray[numpy.integer[Any]]
57
+ foldsTotal: numpy.ndarray[numpy.int64, numpy.dtype[numpy.int64]]
58
+ mapShape: Tuple[int, ...]
59
+ my: numpy.typing.NDArray[numpy.integer[Any]]
60
+ gapsWhere: numpy.typing.NDArray[numpy.integer[Any]]
61
+ the: numpy.typing.NDArray[numpy.integer[Any]]
62
+ track: numpy.typing.NDArray[numpy.integer[Any]]
@@ -0,0 +1,170 @@
1
+ Metadata-Version: 2.2
2
+ Name: mapFolding
3
+ Version: 0.2.0
4
+ Summary: Algorithm(s) for counting distinct ways to fold a map (or a strip of stamps)
5
+ Author-email: Hunter Hogan <HunterHogan@pm.me>
6
+ Project-URL: homepage, https://github.com/hunterhogan/mapFolding
7
+ Requires-Python: <3.13,>=3.10
8
+ Description-Content-Type: text/markdown
9
+ Requires-Dist: numba
10
+ Requires-Dist: numpy
11
+ Requires-Dist: Z0Z-tools
12
+ Provides-Extra: benchmark
13
+ Requires-Dist: pandas; extra == "benchmark"
14
+ Requires-Dist: jupyter; extra == "benchmark"
15
+ Requires-Dist: ipywidgets; extra == "benchmark"
16
+ Requires-Dist: tqdm; extra == "benchmark"
17
+ Provides-Extra: jax
18
+ Requires-Dist: jax; extra == "jax"
19
+ Requires-Dist: jaxtyping; extra == "jax"
20
+ Provides-Extra: testing
21
+ Requires-Dist: pytest; extra == "testing"
22
+ Requires-Dist: pytest-cov; extra == "testing"
23
+ Requires-Dist: pytest-env; extra == "testing"
24
+ Requires-Dist: pytest-xdist; extra == "testing"
25
+
26
+ # Algorithm(s) for counting distinct ways to fold a map (or a strip of stamps)
27
+
28
+ `mapFolding.countFolds()` will accept arbitrary values for the map's dimensions.
29
+
30
+ ```python
31
+ from mapFolding import countFolds
32
+ foldsTotal = countFolds( [2,10] )
33
+ ```
34
+
35
+ The directory `mapFolding/reference` has
36
+
37
+ - a verbatim transcription of the "procedure" published in _The Computer Journal_,
38
+ - multiple referential versions of the procedure with explanatory comments including
39
+ - `hunterNumba.py` a one-size-fits-all, self-contained, reasonably fast, contemporary algorithm that is nevertheless infected by _noobaceae ignorancium_, and
40
+ - miscellaneous notes.
41
+
42
+ [![Python Tests](https://github.com/hunterhogan/mapFolding/actions/workflows/unittests.yml/badge.svg)](https://github.com/hunterhogan/mapFolding/actions/workflows/unittests.yml)
43
+
44
+ ## Simple, easy usage based on OEIS IDs
45
+
46
+ `mapFolding` directly implements some IDs from _The On-Line Encyclopedia of Integer Sequences_.
47
+
48
+ ### Usage: command line
49
+
50
+ After installing (see below), `OEIS_for_n` will run a computation from the command line.
51
+
52
+ ```cmd
53
+ (mapFolding) C:\apps\mapFolding> OEIS_for_n A001418 5
54
+ 186086600 distinct folding patterns.
55
+ Time elapsed: 1.605 seconds
56
+ ```
57
+
58
+ Use `getOEISids` to get the most up-to-date list of available OEIS IDs.
59
+
60
+ ```cmd
61
+ (mapFolding) C:\apps\mapFolding> getOEISids
62
+
63
+ Available OEIS sequences:
64
+ A001415: Number of ways of folding a 2 X n strip of stamps.
65
+ A001416: Number of ways of folding a 3 X n strip of stamps.
66
+ A001417: Number of ways of folding a 2 X 2 X ... X 2 n-dimensional map.
67
+ A001418: Number of ways of folding an n X n sheet of stamps.
68
+ A195646: Number of ways of folding a 3 X 3 X ... X 3 n-dimensional map.
69
+
70
+ Usage examples:
71
+ Command line:
72
+ OEIS_for_n A001415 8
73
+ Python:
74
+ from mapFolding import oeisIDfor_n
75
+ foldsTotal = oeisIDfor_n('A001415', 8)
76
+ ```
77
+
78
+ ### Usage: Python module or REPL
79
+
80
+ Use `mapFolding.oeisIDfor_n()` to compute a(n) for an OEIS ID.
81
+
82
+ ```python
83
+ from mapFolding import oeisIDfor_n
84
+ foldsTotal = oeisIDfor_n( 'A001418', 4 )
85
+ ```
86
+
87
+ ### An "advanced" and likely unnecessary feature: clear `mapFolding`'s cache of OEIS data
88
+
89
+ Clear _The On-Line Encyclopedia of Integer Sequences_ data from the `mapFolding` cache:
90
+
91
+ ```sh
92
+ (mapFolding) C:\apps\mapFolding> clearOEIScache
93
+ Cache cleared from C:\apps\mapFolding\mapFolding\.cache
94
+ ```
95
+
96
+ ## Connections to "Multi-dimensional map-folding" by W. F. Lunnon
97
+
98
+ ### The typo-laden algorithm published in 1971
99
+
100
+ The full paper, W. F. Lunnon, Multi-dimensional map-folding, _The Computer Journal_, Volume 14, Issue 1, 1971, Pages 75–80, [https://doi.org/10.1093/comjnl/14.1.75](https://doi.org/10.1093/comjnl/14.1.75) ([BibTex](mapFolding/citations/Lunnon.bibtex) citation) is available at the DOI link. (As of 3 January 2025, the paper is a PDF of images, not text, and can be accessed without cost or login.)
101
+
102
+ In [`foldings.txt`](mapFolding/reference/foldings.txt), you can find a text transcription of the algorithm as it was printed in 1971. In [`foldings.AA`](mapFolding/reference/foldings.AA), I have corrected obvious transcription errors, documented with comments, and I have reformatted line breaks and indentation. For contemporary readers, the result is likely easier to read than the text transcription or the original paper are easy to read. This is especially true if you view the document with semantic highlighting, such as with [Algol 60 syntax highlighter](https://github.com/PolariTOON/language-algol60).
103
+
104
+ ### Java implementation(s) and improvements
105
+
106
+ [archmageirvine](https://github.com/archmageirvine/joeis/blob/80e3e844b11f149704acbab520bc3a3a25ac34ff/src/irvine/oeis/a001/A001415.java) ([BibTex](mapFolding/citations/jOEIS.bibtex) citation) says about the Java code:
107
+
108
+ ```java
109
+ /**
110
+ * A001415 Number of ways of folding a 2 X n strip of stamps.
111
+ * @author Fred Lunnon (ALGOL68, C versions)
112
+ * @author Sean A. Irvine (Java port)
113
+ */
114
+ ...
115
+ // Implements algorithm as described in "Multi-dimensional map-folding",
116
+ // by W. F. Lunnon, The Computer J, 14, 1, pp. 75--80. Note the original
117
+ // paper contains a few omissions, so this actual code is based on a C
118
+ // implementation by Fred Lunnon.
119
+ ```
120
+
121
+ ## Map-folding Video
122
+
123
+ ~~This caused my neurosis:~~ I enjoyed the following video, which is what introduced me to map folding.
124
+
125
+ "How Many Ways Can You Fold a Map?" by Physics for the Birds, 2024 November 13 ([BibTex](mapFolding/citations/Physics_for_the_Birds.bibtex) citation)
126
+
127
+ [![How Many Ways Can You Fold a Map?](https://i.ytimg.com/vi/sfH9uIY3ln4/hq720.jpg)](https://www.youtube.com/watch?v=sfH9uIY3ln4)
128
+
129
+ ## Install this package
130
+
131
+ ### From Github
132
+
133
+ ```sh
134
+ pip install mapFolding@git+https://github.com/hunterhogan/mapFolding.git
135
+ ```
136
+
137
+ ### From a local directory
138
+
139
+ #### Windows
140
+
141
+ ```powershell
142
+ git clone https://github.com/hunterhogan/mapFolding.git \path\to\mapFolding
143
+ pip install mapFolding@file:\path\to\mapFolding
144
+ ```
145
+
146
+ #### POSIX
147
+
148
+ ```bash
149
+ git clone https://github.com/hunterhogan/mapFolding.git /path/to/mapFolding
150
+ pip install mapFolding@file:/path/to/mapFolding
151
+ ```
152
+
153
+ ## Install updates
154
+
155
+ ```sh
156
+ pip install --upgrade mapFolding@git+https://github.com/hunterhogan/mapFolding.git
157
+ ```
158
+
159
+ ## Creating a virtual environment before installation
160
+
161
+ You can isolate `mapFolding` in a virtual environment. For example, use the following commands to create a directory for the virtual environment, activate the virtual environment, and install the package. In the future, you will likely need to activate the virtual environment before using `mapFolding` again. From the command line, in a directory you want to install in.
162
+
163
+ ```sh
164
+ py -m venv mapFolding
165
+ cd mapFolding
166
+ cd Scripts
167
+ activate
168
+ cd ..
169
+ pip install mapFolding@git+https://github.com/hunterhogan/mapFolding.git
170
+ ```
@@ -0,0 +1,28 @@
1
+ mapFolding/__init__.py,sha256=fQGYxmjMofT1id_svln0L3htMBVUbMVj1UWIP74QhAQ,974
2
+ mapFolding/babbage.py,sha256=tmz4ZEvMds1nPK7kb_oyl89AsGyuU0MB5udC8AAIL-I,633
3
+ mapFolding/beDRY.py,sha256=JUGAuLpQYD-B_mSrgpw4E-JNM_N3gJ2VfY-FkrBP3yA,11135
4
+ mapFolding/importPackages.py,sha256=Pno5VXaNiyJKG2Jtj4sB_h6EG50IJ7tQKyivVoKFMxo,303
5
+ mapFolding/lovelace.py,sha256=NdQDPYpe7yU8GS9WIv_Rb4nOE9cPt0-XPKOiLfyE_Z4,9369
6
+ mapFolding/oeis.py,sha256=pI08RrA39AVAU7U6h_1IowadxXC4K6n2OQd7vgTbTTI,11096
7
+ mapFolding/startHere.py,sha256=37wU-VmM92bOZkFw4Wpiz3u0_1TiLsob94HZUPBwFQg,2433
8
+ mapFolding/theSSOT.py,sha256=-t23-gPLPNWWBEeSi1mKkNCeeQA4y1AA_oPKC7tZwe4,1970
9
+ mapFolding/JAX/lunnanJAX.py,sha256=xMZloN47q-MVfjdYOM1hi9qR4OnLq7qALmGLMraevQs,14819
10
+ mapFolding/JAX/taskJAX.py,sha256=yJNeH0rL6EhJ6ppnATHF0Zf81CDMC10bnPnimVxE1hc,20037
11
+ mapFolding/benchmarks/benchmarking.py,sha256=kv85F6V9pGhZvTOImArOuxyg5rywA_T6JLH_qFXM8BM,3018
12
+ mapFolding/benchmarks/test_benchmarks.py,sha256=c4ANeR3jgqpKXFoxDeZkmAHxSuenMwsjmrhKJ1_XPqY,3659
13
+ mapFolding/reference/hunterNumba.py,sha256=jV0dBjhLcnJjUHWRxLV_xvPIUP_GRLRKVeDhOtM9Wp4,7475
14
+ mapFolding/reference/irvineJavaPort.py,sha256=Sj-63Z-OsGuDoEBXuxyjRrNmmyl0d7Yz_XuY7I47Oyg,4250
15
+ mapFolding/reference/lunnan.py,sha256=SwjpOr1ROg-yZaZuiL7XjXhuvBxu_K4gQoPAWKUjJk4,5037
16
+ mapFolding/reference/lunnanNumpy.py,sha256=bDQsBZUQcqJ9bTdnMF7HgtPcgpIodctujkm_lpq9NZ4,4653
17
+ mapFolding/reference/lunnanWhile.py,sha256=p8_zLdMc6ujTYIu_yn-hbzOuLTQHvo0ENciIDmKayDU,4067
18
+ mapFolding/reference/rotatedEntryPoint.py,sha256=6WVvEcGwDgRPa7dDs7ODAHUJjHDZDIDN6CXH8fFVFmA,12105
19
+ tests/__init__.py,sha256=PGYVr7r23gATgcvZ3Sfph9D_g1MVvhgzMNWXBs_9tmY,52
20
+ tests/conftest.py,sha256=ur6l5nDnWju7zOEMXqoNGYHY-Hf9GApOm4VT9pXtMtY,10594
21
+ tests/test_oeis.py,sha256=h-NhWRoarl3v6qoB0RpnaUPuMGrQpWyfmsL0FXsGauU,7972
22
+ tests/test_other.py,sha256=AZYRvZYL-wdDiKcbAFEBTV4a22CANmbl8mj8yVGBQ6g,4852
23
+ tests/test_tasks.py,sha256=bBXsMfxb6WXz15eFjUxoS67wzRqusX_9XhetXaY-RnQ,944
24
+ mapFolding-0.2.0.dist-info/METADATA,sha256=EC2InFN5XJ1LqKGptTerqGvuJQntDQdyMFG4WIvR6WU,6602
25
+ mapFolding-0.2.0.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
26
+ mapFolding-0.2.0.dist-info/entry_points.txt,sha256=F3OUeZR1XDTpoH7k3wXuRb3KF_kXTTeYhu5AGK1SiOQ,146
27
+ mapFolding-0.2.0.dist-info/top_level.txt,sha256=1gP2vFaqPwHujGwb3UjtMlLEGN-943VSYFR7V4gDqW8,17
28
+ mapFolding-0.2.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (75.8.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,4 @@
1
+ [console_scripts]
2
+ OEIS_for_n = mapFolding.oeis:OEIS_for_n
3
+ clearOEIScache = mapFolding.oeis:clearOEIScache
4
+ getOEISids = mapFolding.oeis:getOEISids
@@ -0,0 +1,2 @@
1
+ mapFolding
2
+ tests
tests/__init__.py ADDED
@@ -0,0 +1 @@
1
+ from .conftest import makeDictionaryFoldsTotalKnown
tests/conftest.py ADDED
@@ -0,0 +1,262 @@
1
+ """SSOT for Pytest.
2
+ Other test modules must not import directly from the package being tested."""
3
+
4
+ # TODO learn how to run tests and coverage analysis without `env = ["NUMBA_DISABLE_JIT=1"]`
5
+
6
+ from typing import Any, Callable, Dict, Generator, List, Optional, Sequence, Tuple, Type, Union
7
+ import pathlib
8
+ import pytest
9
+ import random
10
+ import unittest.mock
11
+
12
+ from mapFolding import clearOEIScache
13
+ from mapFolding import countFolds, pathJobDEFAULT
14
+ from mapFolding import getLeavesTotal, parseDimensions, validateListDimensions
15
+ from mapFolding.importPackages import makeTestSuiteConcurrencyLimit, defineConcurrencyLimit
16
+ from mapFolding.importPackages import makeTestSuiteIntInnit, intInnit
17
+ from mapFolding.importPackages import makeTestSuiteOopsieKwargsie, oopsieKwargsie
18
+ from mapFolding.oeis import OEIS_for_n
19
+ from mapFolding.oeis import _formatFilenameCache
20
+ from mapFolding.oeis import _getOEISidValues
21
+ from mapFolding.oeis import _parseBFileOEIS
22
+ from mapFolding.oeis import _validateOEISid
23
+ from mapFolding.oeis import getOEISids
24
+ from mapFolding.oeis import oeisIDfor_n
25
+ from mapFolding.oeis import oeisIDsImplemented
26
+ from mapFolding.oeis import settingsOEIS
27
+
28
+ __all__ = [
29
+ 'OEIS_for_n',
30
+ '_formatFilenameCache',
31
+ '_getOEISidValues',
32
+ '_parseBFileOEIS',
33
+ '_validateOEISid',
34
+ 'clearOEIScache',
35
+ 'countFolds',
36
+ 'defineConcurrencyLimit',
37
+ 'expectSystemExit',
38
+ 'getLeavesTotal',
39
+ 'getOEISids',
40
+ 'intInnit',
41
+ 'makeTestSuiteConcurrencyLimit',
42
+ 'makeTestSuiteIntInnit',
43
+ 'makeTestSuiteOopsieKwargsie',
44
+ 'oeisIDfor_n',
45
+ 'oeisIDsImplemented',
46
+ 'oopsieKwargsie',
47
+ 'parseDimensions',
48
+ 'settingsOEIS',
49
+ 'standardCacheTest',
50
+ 'standardComparison',
51
+ 'validateListDimensions',
52
+ ]
53
+
54
+ def makeDictionaryFoldsTotalKnown() -> Dict[Tuple[int,...], int]:
55
+ """Returns a dictionary mapping dimension tuples to their known folding totals."""
56
+ dictionaryMapDimensionsToFoldsTotalKnown = {}
57
+
58
+ for settings in settingsOEIS.values():
59
+ sequence = settings['valuesKnown']
60
+
61
+ for n, foldingsTotal in sequence.items():
62
+ dimensions = settings['getDimensions'](n)
63
+ dimensions.sort()
64
+ dictionaryMapDimensionsToFoldsTotalKnown[tuple(dimensions)] = foldingsTotal
65
+
66
+ # Are we in a place that has jobs?
67
+ if pathJobDEFAULT.exists():
68
+ # Are there foldsTotal files?
69
+ for pathFilenameFoldsTotal in pathJobDEFAULT.rglob('*.foldsTotal'):
70
+ if pathFilenameFoldsTotal.is_file():
71
+ try:
72
+ listDimensions = eval(pathFilenameFoldsTotal.stem)
73
+ except Exception:
74
+ continue
75
+ # Are the dimensions in the dictionary?
76
+ if isinstance(listDimensions, list) and all(isinstance(dimension, int) for dimension in listDimensions):
77
+ listDimensions.sort()
78
+ if tuple(listDimensions) in dictionaryMapDimensionsToFoldsTotalKnown:
79
+ continue
80
+ # Are the contents a reasonably large integer?
81
+ try:
82
+ foldsTotal = pathFilenameFoldsTotal.read_text()
83
+ except Exception:
84
+ continue
85
+ # Why did I sincerely believe this would only be three lines of code?
86
+ if foldsTotal.isdigit() and int(foldsTotal) > 85109616 * 10**3:
87
+ foldsTotal = int(foldsTotal)
88
+ # You made it this far, so fuck it: put it in the dictionary
89
+ dictionaryMapDimensionsToFoldsTotalKnown[tuple(listDimensions)] = foldsTotal
90
+ # The sunk-costs fallacy claims another victim!
91
+
92
+ return dictionaryMapDimensionsToFoldsTotalKnown
93
+
94
+ """
95
+ Section: Fixtures"""
96
+
97
+ @pytest.fixture
98
+ def foldsTotalKnown() -> Dict[Tuple[int,...], int]:
99
+ """Returns a dictionary mapping dimension tuples to their known folding totals.
100
+ NOTE I am not convinced this is the best way to do this.
101
+ Advantage: I call `makeDictionaryFoldsTotalKnown()` from modules other than test modules.
102
+ Preference: I _think_ I would prefer a SSOT function available to any module
103
+ similar to `foldsTotalKnown = getFoldsTotalKnown(listDimensions)`."""
104
+ return makeDictionaryFoldsTotalKnown()
105
+
106
+ @pytest.fixture
107
+ def listDimensionsTestFunctionality(oeisID_1random: str) -> List[int]:
108
+ """To test functionality, get one `listDimensions` from `valuesTestValidation` if
109
+ `validateListDimensions` approves. The algorithm can count the folds of the returned
110
+ `listDimensions` in a short enough time suitable for testing."""
111
+ while True:
112
+ n = random.choice(settingsOEIS[oeisID_1random]['valuesTestValidation'])
113
+ if n < 2:
114
+ continue
115
+ listDimensionsCandidate = settingsOEIS[oeisID_1random]['getDimensions'](n)
116
+
117
+ try:
118
+ return validateListDimensions(listDimensionsCandidate)
119
+ except (ValueError, NotImplementedError):
120
+ pass
121
+
122
+ @pytest.fixture
123
+ def listDimensionsTest_countFolds(oeisID: str) -> List[int]:
124
+ """For each `oeisID` from the `pytest.fixture`, returns `listDimensions` from `valuesTestValidation`
125
+ if `validateListDimensions` approves. Each `listDimensions` is suitable for testing counts."""
126
+ while True:
127
+ n = random.choice(settingsOEIS[oeisID]['valuesTestValidation'])
128
+ if n < 2:
129
+ continue
130
+ listDimensionsCandidate = settingsOEIS[oeisID]['getDimensions'](n)
131
+
132
+ try:
133
+ return validateListDimensions(listDimensionsCandidate)
134
+ except (ValueError, NotImplementedError):
135
+ pass
136
+
137
+ @pytest.fixture
138
+ def mockBenchmarkTimer() -> Generator[unittest.mock.MagicMock | unittest.mock.AsyncMock, Any, None]:
139
+ """Mock time.perf_counter_ns for consistent benchmark timing."""
140
+ with unittest.mock.patch('time.perf_counter_ns') as mockTimer:
141
+ mockTimer.side_effect = [0, 1e9] # Start and end times for 1 second
142
+ yield mockTimer
143
+
144
+ @pytest.fixture(params=oeisIDsImplemented)
145
+ def oeisID(request: pytest.FixtureRequest)-> str:
146
+ return request.param
147
+
148
+ @pytest.fixture
149
+ def oeisID_1random() -> str:
150
+ """Return one random valid OEIS ID."""
151
+ return random.choice(oeisIDsImplemented)
152
+
153
+ @pytest.fixture
154
+ def pathCacheTesting(tmp_path: pathlib.Path) -> Generator[pathlib.Path, Any, None]:
155
+ """Temporarily replace the OEIS cache directory with a test directory."""
156
+ from mapFolding import oeis as there_must_be_a_better_way
157
+ pathCacheOriginal = there_must_be_a_better_way._pathCache
158
+ there_must_be_a_better_way._pathCache = tmp_path
159
+ yield tmp_path
160
+ there_must_be_a_better_way._pathCache = pathCacheOriginal
161
+
162
+ @pytest.fixture
163
+ def pathBenchmarksTesting(tmp_path: pathlib.Path) -> Generator[pathlib.Path, Any, None]:
164
+ """Temporarily replace the benchmarks directory with a test directory."""
165
+ from mapFolding.benchmarks import benchmarking
166
+ pathOriginal = benchmarking.pathFilenameRecordedBenchmarks
167
+ pathTest = tmp_path / "benchmarks.npy"
168
+ benchmarking.pathFilenameRecordedBenchmarks = pathTest
169
+ yield pathTest
170
+ benchmarking.pathFilenameRecordedBenchmarks = pathOriginal
171
+
172
+ """
173
+ Section: Standardized test structures"""
174
+
175
+ def standardComparison(expected: Any, functionTarget: Callable, *arguments: Any) -> None:
176
+ """Template for tests expecting an error."""
177
+ if type(expected) == Type[Exception]:
178
+ messageExpected = expected.__name__
179
+ else:
180
+ messageExpected = expected
181
+
182
+ try:
183
+ messageActual = actual = functionTarget(*arguments)
184
+ except Exception as actualError:
185
+ messageActual = type(actualError).__name__
186
+ actual = type(actualError)
187
+
188
+ assert actual == expected, formatTestMessage(messageExpected, messageActual, functionTarget.__name__, *arguments)
189
+
190
+ def expectSystemExit(expected: Union[str, int, Sequence[int]], functionTarget: Callable, *arguments: Any) -> None:
191
+ """Template for tests expecting SystemExit.
192
+
193
+ Parameters
194
+ expected: Exit code expectation:
195
+ - "error": any non-zero exit code
196
+ - "nonError": specifically zero exit code
197
+ - int: exact exit code match
198
+ - Sequence[int]: exit code must be one of these values
199
+ functionTarget: The function to test
200
+ arguments: Arguments to pass to the function
201
+ """
202
+ with pytest.raises(SystemExit) as exitInfo:
203
+ functionTarget(*arguments)
204
+
205
+ exitCode = exitInfo.value.code
206
+
207
+ if expected == "error":
208
+ assert exitCode != 0, \
209
+ f"Expected error exit (non-zero) but got code {exitCode}"
210
+ elif expected == "nonError":
211
+ assert exitCode == 0, \
212
+ f"Expected non-error exit (0) but got code {exitCode}"
213
+ elif isinstance(expected, (list, tuple)):
214
+ assert exitCode in expected, \
215
+ f"Expected exit code to be one of {expected} but got {exitCode}"
216
+ else:
217
+ assert exitCode == expected, \
218
+ f"Expected exit code {expected} but got {exitCode}"
219
+
220
+ def formatTestMessage(expected: Any, actual: Any, functionName: str, *arguments: Any) -> str:
221
+ """Format assertion message for any test comparison."""
222
+ return (f"\nTesting: `{functionName}({', '.join(str(parameter) for parameter in arguments)})`\n"
223
+ f"Expected: {expected}\n"
224
+ f"Got: {actual}")
225
+
226
+ def standardCacheTest(
227
+ expected: Any,
228
+ setupCacheFile: Optional[Callable[[pathlib.Path, str], None]],
229
+ oeisID: str,
230
+ pathCache: pathlib.Path
231
+ ) -> None:
232
+ """Template for tests involving OEIS cache operations.
233
+
234
+ Parameters
235
+ expected: Expected value or exception from _getOEISidValues
236
+ setupCacheFile: Function to prepare the cache file before test
237
+ oeisID: OEIS ID to test
238
+ pathCache: Temporary cache directory path
239
+ """
240
+ pathFilenameCache = pathCache / _formatFilenameCache.format(oeisID=oeisID)
241
+
242
+ # Setup cache file if provided
243
+ if setupCacheFile:
244
+ setupCacheFile(pathFilenameCache, oeisID)
245
+
246
+ # Run test
247
+ try:
248
+ actual = _getOEISidValues(oeisID)
249
+ messageActual = actual
250
+ except Exception as actualError:
251
+ actual = type(actualError)
252
+ messageActual = type(actualError).__name__
253
+
254
+ # Compare results
255
+ if isinstance(expected, type) and issubclass(expected, Exception):
256
+ messageExpected = expected.__name__
257
+ assert isinstance(actual, expected), formatTestMessage(
258
+ messageExpected, messageActual, "_getOEISidValues", oeisID)
259
+ else:
260
+ messageExpected = expected
261
+ assert actual == expected, formatTestMessage(
262
+ messageExpected, messageActual, "_getOEISidValues", oeisID)