sciris 3.4.0__tar.gz → 3.5.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.
Files changed (173) hide show
  1. {sciris-3.4.0 → sciris-3.5.0}/CHANGELOG.md +8 -0
  2. {sciris-3.4.0 → sciris-3.5.0}/CLAUDE.md +1 -1
  3. {sciris-3.4.0/sciris.egg-info → sciris-3.5.0}/PKG-INFO +1 -1
  4. {sciris-3.4.0 → sciris-3.5.0}/docs/.gitignore +5 -0
  5. {sciris-3.4.0 → sciris-3.5.0}/docs/index.qmd +1 -1
  6. {sciris-3.4.0 → sciris-3.5.0}/docs/make_api.py +4 -47
  7. {sciris-3.4.0 → sciris-3.5.0}/docs/tutorials/tut_dicts.qmd +13 -1
  8. sciris-3.5.0/objdict_performance.md +89 -0
  9. {sciris-3.4.0 → sciris-3.5.0}/sciris/sc_nested.py +2 -1
  10. {sciris-3.4.0 → sciris-3.5.0}/sciris/sc_odict.py +63 -69
  11. {sciris-3.4.0 → sciris-3.5.0}/sciris/sc_version.py +2 -2
  12. {sciris-3.4.0 → sciris-3.5.0/sciris.egg-info}/PKG-INFO +1 -1
  13. {sciris-3.4.0 → sciris-3.5.0}/sciris.egg-info/SOURCES.txt +1 -3
  14. {sciris-3.4.0 → sciris-3.5.0}/tests/test_api.py +1 -16
  15. {sciris-3.4.0 → sciris-3.5.0}/tests/test_odict.py +7 -0
  16. sciris-3.4.0/docs/api.json +0 -2824
  17. sciris-3.4.0/docs/llms-full.txt +0 -2120
  18. sciris-3.4.0/docs/llms.txt +0 -342
  19. {sciris-3.4.0 → sciris-3.5.0}/.claude-plugin/marketplace.json +0 -0
  20. {sciris-3.4.0 → sciris-3.5.0}/.github/workflows/publish_docs.yaml +0 -0
  21. {sciris-3.4.0 → sciris-3.5.0}/.github/workflows/pypi_release.yaml +0 -0
  22. {sciris-3.4.0 → sciris-3.5.0}/.github/workflows/schedule_tests.yaml +0 -0
  23. {sciris-3.4.0 → sciris-3.5.0}/.github/workflows/test_docs.yaml +0 -0
  24. {sciris-3.4.0 → sciris-3.5.0}/.github/workflows/test_downstream.yaml +0 -0
  25. {sciris-3.4.0 → sciris-3.5.0}/.github/workflows/test_sciris.yaml +0 -0
  26. {sciris-3.4.0 → sciris-3.5.0}/.gitignore +0 -0
  27. {sciris-3.4.0 → sciris-3.5.0}/.pylintrc +0 -0
  28. {sciris-3.4.0 → sciris-3.5.0}/.pytest_cache/README.md +0 -0
  29. {sciris-3.4.0 → sciris-3.5.0}/CODE_OF_CONDUCT.md +0 -0
  30. {sciris-3.4.0 → sciris-3.5.0}/CONTRIBUTING.md +0 -0
  31. {sciris-3.4.0 → sciris-3.5.0}/LICENSE +0 -0
  32. {sciris-3.4.0 → sciris-3.5.0}/MANIFEST.in +0 -0
  33. {sciris-3.4.0 → sciris-3.5.0}/README.md +0 -0
  34. {sciris-3.4.0 → sciris-3.5.0}/STYLE_GUIDE.md +0 -0
  35. {sciris-3.4.0 → sciris-3.5.0}/claude_plugin/.claude-plugin/plugin.json +0 -0
  36. {sciris-3.4.0 → sciris-3.5.0}/claude_plugin/README.md +0 -0
  37. {sciris-3.4.0 → sciris-3.5.0}/claude_plugin/skills/sciris-advanced/SKILL.md +0 -0
  38. {sciris-3.4.0 → sciris-3.5.0}/claude_plugin/skills/sciris-arrays/SKILL.md +0 -0
  39. {sciris-3.4.0 → sciris-3.5.0}/claude_plugin/skills/sciris-dates/SKILL.md +0 -0
  40. {sciris-3.4.0 → sciris-3.5.0}/claude_plugin/skills/sciris-dicts/SKILL.md +0 -0
  41. {sciris-3.4.0 → sciris-3.5.0}/claude_plugin/skills/sciris-files/SKILL.md +0 -0
  42. {sciris-3.4.0 → sciris-3.5.0}/claude_plugin/skills/sciris-intro/SKILL.md +0 -0
  43. {sciris-3.4.0 → sciris-3.5.0}/claude_plugin/skills/sciris-parallel/SKILL.md +0 -0
  44. {sciris-3.4.0 → sciris-3.5.0}/claude_plugin/skills/sciris-plotting/SKILL.md +0 -0
  45. {sciris-3.4.0 → sciris-3.5.0}/claude_plugin/skills/sciris-printing/SKILL.md +0 -0
  46. {sciris-3.4.0 → sciris-3.5.0}/claude_plugin/skills/sciris-utils/SKILL.md +0 -0
  47. {sciris-3.4.0 → sciris-3.5.0}/context7.json +0 -0
  48. {sciris-3.4.0 → sciris-3.5.0}/docs/404.qmd +0 -0
  49. {sciris-3.4.0 → sciris-3.5.0}/docs/CNAME +0 -0
  50. {sciris-3.4.0 → sciris-3.5.0}/docs/README.md +0 -0
  51. {sciris-3.4.0 → sciris-3.5.0}/docs/_extensions/machow/interlinks/objects.txt +0 -0
  52. {sciris-3.4.0 → sciris-3.5.0}/docs/_quarto.yml +0 -0
  53. {sciris-3.4.0 → sciris-3.5.0}/docs/_site/llms-full.txt +0 -0
  54. {sciris-3.4.0 → sciris-3.5.0}/docs/_site/llms.txt +0 -0
  55. {sciris-3.4.0 → sciris-3.5.0}/docs/_site/robots.txt +0 -0
  56. {sciris-3.4.0 → sciris-3.5.0}/docs/_variables.yml +0 -0
  57. {sciris-3.4.0 → sciris-3.5.0}/docs/api/index.qmd +0 -0
  58. {sciris-3.4.0 → sciris-3.5.0}/docs/assets/favicon.ico +0 -0
  59. {sciris-3.4.0 → sciris-3.5.0}/docs/assets/sciris-logo-dark.png +0 -0
  60. {sciris-3.4.0 → sciris-3.5.0}/docs/assets/sciris-logo-grey.png +0 -0
  61. {sciris-3.4.0 → sciris-3.5.0}/docs/assets/sciris-logo-large.png +0 -0
  62. {sciris-3.4.0 → sciris-3.5.0}/docs/assets/sciris-logo.png +0 -0
  63. {sciris-3.4.0 → sciris-3.5.0}/docs/assets/sciris-showcase-code.png +0 -0
  64. {sciris-3.4.0 → sciris-3.5.0}/docs/assets/sciris-showcase-output.png +0 -0
  65. {sciris-3.4.0 → sciris-3.5.0}/docs/assets/styles-dark.scss +0 -0
  66. {sciris-3.4.0 → sciris-3.5.0}/docs/assets/styles-light.scss +0 -0
  67. {sciris-3.4.0 → sciris-3.5.0}/docs/assets/styles.css +0 -0
  68. {sciris-3.4.0 → sciris-3.5.0}/docs/check_notebooks.py +0 -0
  69. {sciris-3.4.0 → sciris-3.5.0}/docs/clean_all +0 -0
  70. {sciris-3.4.0 → sciris-3.5.0}/docs/clean_outputs.py +0 -0
  71. {sciris-3.4.0 → sciris-3.5.0}/docs/conduct.qmd +0 -0
  72. {sciris-3.4.0 → sciris-3.5.0}/docs/contributing.qmd +0 -0
  73. {sciris-3.4.0 → sciris-3.5.0}/docs/overview.qmd +0 -0
  74. {sciris-3.4.0 → sciris-3.5.0}/docs/preview +0 -0
  75. {sciris-3.4.0 → sciris-3.5.0}/docs/publish +0 -0
  76. {sciris-3.4.0 → sciris-3.5.0}/docs/quarto_utils.py +0 -0
  77. {sciris-3.4.0 → sciris-3.5.0}/docs/render +0 -0
  78. {sciris-3.4.0 → sciris-3.5.0}/docs/requirements.txt +0 -0
  79. {sciris-3.4.0 → sciris-3.5.0}/docs/sciris-citation.bib +0 -0
  80. {sciris-3.4.0 → sciris-3.5.0}/docs/style_guide.qmd +0 -0
  81. {sciris-3.4.0 → sciris-3.5.0}/docs/tutorials/.jupyter_cache/__version__.txt +0 -0
  82. {sciris-3.4.0 → sciris-3.5.0}/docs/tutorials/README.md +0 -0
  83. {sciris-3.4.0 → sciris-3.5.0}/docs/tutorials/convert_notebooks +0 -0
  84. {sciris-3.4.0 → sciris-3.5.0}/docs/tutorials/dancing_lines.gif +0 -0
  85. {sciris-3.4.0 → sciris-3.5.0}/docs/tutorials/img-advanced.gif +0 -0
  86. {sciris-3.4.0 → sciris-3.5.0}/docs/tutorials/img-arrays.png +0 -0
  87. {sciris-3.4.0 → sciris-3.5.0}/docs/tutorials/img-dates.png +0 -0
  88. {sciris-3.4.0 → sciris-3.5.0}/docs/tutorials/img-dicts.png +0 -0
  89. {sciris-3.4.0 → sciris-3.5.0}/docs/tutorials/img-files.png +0 -0
  90. {sciris-3.4.0 → sciris-3.5.0}/docs/tutorials/img-intro.png +0 -0
  91. {sciris-3.4.0 → sciris-3.5.0}/docs/tutorials/img-parallel.png +0 -0
  92. {sciris-3.4.0 → sciris-3.5.0}/docs/tutorials/img-plotting.png +0 -0
  93. {sciris-3.4.0 → sciris-3.5.0}/docs/tutorials/img-printing.png +0 -0
  94. {sciris-3.4.0 → sciris-3.5.0}/docs/tutorials/img-utils.png +0 -0
  95. {sciris-3.4.0 → sciris-3.5.0}/docs/tutorials/sim1/sim.py +0 -0
  96. {sciris-3.4.0 → sciris-3.5.0}/docs/tutorials/sim2/sim.py +0 -0
  97. {sciris-3.4.0 → sciris-3.5.0}/docs/tutorials/tut_advanced.qmd +0 -0
  98. {sciris-3.4.0 → sciris-3.5.0}/docs/tutorials/tut_arrays.qmd +0 -0
  99. {sciris-3.4.0 → sciris-3.5.0}/docs/tutorials/tut_dates.qmd +0 -0
  100. {sciris-3.4.0 → sciris-3.5.0}/docs/tutorials/tut_files.qmd +0 -0
  101. {sciris-3.4.0 → sciris-3.5.0}/docs/tutorials/tut_intro.qmd +0 -0
  102. {sciris-3.4.0 → sciris-3.5.0}/docs/tutorials/tut_parallel.qmd +0 -0
  103. {sciris-3.4.0 → sciris-3.5.0}/docs/tutorials/tut_plotting.qmd +0 -0
  104. {sciris-3.4.0 → sciris-3.5.0}/docs/tutorials/tut_printing.qmd +0 -0
  105. {sciris-3.4.0 → sciris-3.5.0}/docs/tutorials/tut_utils.qmd +0 -0
  106. {sciris-3.4.0 → sciris-3.5.0}/docs/tutorials.qmd +0 -0
  107. {sciris-3.4.0 → sciris-3.5.0}/docs/whatsnew.qmd +0 -0
  108. {sciris-3.4.0 → sciris-3.5.0}/pyproject.toml +0 -0
  109. {sciris-3.4.0 → sciris-3.5.0}/sciris/README.md +0 -0
  110. {sciris-3.4.0 → sciris-3.5.0}/sciris/__init__.py +0 -0
  111. {sciris-3.4.0 → sciris-3.5.0}/sciris/_extras/README.md +0 -0
  112. {sciris-3.4.0 → sciris-3.5.0}/sciris/_extras/__init__.py +0 -0
  113. {sciris-3.4.0 → sciris-3.5.0}/sciris/_extras/ansicolors.py +0 -0
  114. {sciris-3.4.0 → sciris-3.5.0}/sciris/fancy.mplstyle +0 -0
  115. {sciris-3.4.0 → sciris-3.5.0}/sciris/sc_asd.py +0 -0
  116. {sciris-3.4.0 → sciris-3.5.0}/sciris/sc_colors.py +0 -0
  117. {sciris-3.4.0 → sciris-3.5.0}/sciris/sc_dataframe.py +0 -0
  118. {sciris-3.4.0 → sciris-3.5.0}/sciris/sc_datetime.py +0 -0
  119. {sciris-3.4.0 → sciris-3.5.0}/sciris/sc_fileio.py +0 -0
  120. {sciris-3.4.0 → sciris-3.5.0}/sciris/sc_math.py +0 -0
  121. {sciris-3.4.0 → sciris-3.5.0}/sciris/sc_parallel.py +0 -0
  122. {sciris-3.4.0 → sciris-3.5.0}/sciris/sc_plotting.py +0 -0
  123. {sciris-3.4.0 → sciris-3.5.0}/sciris/sc_printing.py +0 -0
  124. {sciris-3.4.0 → sciris-3.5.0}/sciris/sc_profiling.py +0 -0
  125. {sciris-3.4.0 → sciris-3.5.0}/sciris/sc_settings.py +0 -0
  126. {sciris-3.4.0 → sciris-3.5.0}/sciris/sc_utils.py +0 -0
  127. {sciris-3.4.0 → sciris-3.5.0}/sciris/sc_versioning.py +0 -0
  128. {sciris-3.4.0 → sciris-3.5.0}/sciris/simple.mplstyle +0 -0
  129. {sciris-3.4.0 → sciris-3.5.0}/sciris.egg-info/dependency_links.txt +0 -0
  130. {sciris-3.4.0 → sciris-3.5.0}/sciris.egg-info/requires.txt +0 -0
  131. {sciris-3.4.0 → sciris-3.5.0}/sciris.egg-info/top_level.txt +0 -0
  132. {sciris-3.4.0 → sciris-3.5.0}/setup.cfg +0 -0
  133. {sciris-3.4.0 → sciris-3.5.0}/tests/.coveragerc +0 -0
  134. {sciris-3.4.0 → sciris-3.5.0}/tests/.pytest_cache/README.md +0 -0
  135. {sciris-3.4.0 → sciris-3.5.0}/tests/README.rst +0 -0
  136. {sciris-3.4.0 → sciris-3.5.0}/tests/check_coverage +0 -0
  137. {sciris-3.4.0 → sciris-3.5.0}/tests/check_style +0 -0
  138. {sciris-3.4.0 → sciris-3.5.0}/tests/files/archive_2023-04-19.zip +0 -0
  139. {sciris-3.4.0 → sciris-3.5.0}/tests/files/deadclass.obj +0 -0
  140. {sciris-3.4.0 → sciris-3.5.0}/tests/files/deadclass.py +0 -0
  141. {sciris-3.4.0 → sciris-3.5.0}/tests/files/exampledata.xlsx +0 -0
  142. {sciris-3.4.0 → sciris-3.5.0}/tests/files/examplefont.ttf +0 -0
  143. {sciris-3.4.0 → sciris-3.5.0}/tests/files/make_archive.py +0 -0
  144. {sciris-3.4.0 → sciris-3.5.0}/tests/files/make_deadclass.py +0 -0
  145. {sciris-3.4.0 → sciris-3.5.0}/tests/files/pickle_2021-01-01.obj +0 -0
  146. {sciris-3.4.0 → sciris-3.5.0}/tests/files/pickle_2022-01-01.obj +0 -0
  147. {sciris-3.4.0 → sciris-3.5.0}/tests/pytest.ini +0 -0
  148. {sciris-3.4.0 → sciris-3.5.0}/tests/regression/README.rst +0 -0
  149. {sciris-3.4.0 → sciris-3.5.0}/tests/regression/run_downstream +0 -0
  150. {sciris-3.4.0 → sciris-3.5.0}/tests/regression/run_regression +0 -0
  151. {sciris-3.4.0 → sciris-3.5.0}/tests/regression/sciris2021-01-01.yml +0 -0
  152. {sciris-3.4.0 → sciris-3.5.0}/tests/regression/sciris2022-01-01.yml +0 -0
  153. {sciris-3.4.0 → sciris-3.5.0}/tests/regression/sciris2023-01-01.yml +0 -0
  154. {sciris-3.4.0 → sciris-3.5.0}/tests/regression/setup_envs +0 -0
  155. {sciris-3.4.0 → sciris-3.5.0}/tests/requirements.txt +0 -0
  156. {sciris-3.4.0 → sciris-3.5.0}/tests/run_tests +0 -0
  157. {sciris-3.4.0 → sciris-3.5.0}/tests/sc_test_utils.py +0 -0
  158. {sciris-3.4.0 → sciris-3.5.0}/tests/showcase_sciris.py +0 -0
  159. {sciris-3.4.0 → sciris-3.5.0}/tests/showcase_vanilla.py +0 -0
  160. {sciris-3.4.0 → sciris-3.5.0}/tests/test_asd.py +0 -0
  161. {sciris-3.4.0 → sciris-3.5.0}/tests/test_colors.py +0 -0
  162. {sciris-3.4.0 → sciris-3.5.0}/tests/test_dataframe.py +0 -0
  163. {sciris-3.4.0 → sciris-3.5.0}/tests/test_datetime.py +0 -0
  164. {sciris-3.4.0 → sciris-3.5.0}/tests/test_fileio.py +0 -0
  165. {sciris-3.4.0 → sciris-3.5.0}/tests/test_math.py +0 -0
  166. {sciris-3.4.0 → sciris-3.5.0}/tests/test_nested.py +0 -0
  167. {sciris-3.4.0 → sciris-3.5.0}/tests/test_parallel.py +0 -0
  168. {sciris-3.4.0 → sciris-3.5.0}/tests/test_plotting.py +0 -0
  169. {sciris-3.4.0 → sciris-3.5.0}/tests/test_printing.py +0 -0
  170. {sciris-3.4.0 → sciris-3.5.0}/tests/test_profiling.py +0 -0
  171. {sciris-3.4.0 → sciris-3.5.0}/tests/test_settings.py +0 -0
  172. {sciris-3.4.0 → sciris-3.5.0}/tests/test_utils.py +0 -0
  173. {sciris-3.4.0 → sciris-3.5.0}/tests/test_versioning.py +0 -0
@@ -4,6 +4,14 @@ All major updates to Sciris are documented here.
4
4
 
5
5
  By import convention, components of the Sciris library are listed beginning with `sc.`, e.g. `sc.odict()`.
6
6
 
7
+ ## Version 3.5.0 (2026-10-13)
8
+
9
+ 1. `sc.obj()` is a new, shorter name for `sc.objdict()`, e.g. `pars = sc.obj(a=1, b=2)`. It is the preferred name for new code.
10
+ 1. `sc.objdict()` is now much faster: reading a key as an attribute (`obj.a`) is about 10 times faster, setting one (`obj.a = 1`) is about 4 times faster, and looking up a method (`obj.items`) is about 3 times faster. Creating an `sc.odict()` or `sc.objdict()` is about twice as fast, and `sc.iterobj()` and `sc.search()` are faster too.
11
+ 1. `sc.dictobj()` now stores its keys in the dict itself, so it works with `json.dumps()` and other functions that expect a plain dict. Item access (`obj['a']`) is about 5 times faster. Files saved with earlier versions can still be loaded.
12
+ 1. As a guide to which to use: `sc.dictobj()` is nearly as fast as a plain `dict` (about 15–20 ns to get or set a key either way, versus 10–15 ns for a `dict`), while `sc.objdict()` takes about 40–50 ns to get a key and 200–300 ns to set one, in exchange for the extra features of `sc.odict()` (such as indexing by position) and for protecting its methods from being overwritten by keys. So use `sc.objdict()` by default, and `sc.dictobj()` or a `dict` in loops that run millions of times.
13
+ 1. For subclasses of `sc.objdict()` with properties: if a property raises an exception, that exception is now always raised (previously, a key with the same name would be returned instead); and if a property fails because an attribute is missing, the error message names the property rather than the missing attribute.
14
+
7
15
  ## Version 3.4.0 (2026-09-28)
8
16
 
9
17
  ### Major changes
@@ -14,7 +14,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
14
14
  - **Run tests:** `cd tests && ./run_tests`
15
15
  - **Install dev:** `pip install -e .`
16
16
  - **Build docs:** `cd docs && ./render`
17
- - **Regenerate API index:** `cd docs && python make_api.py` (updates `docs/api.json`, `docs/llms.txt`, `docs/llms-full.txt`; run after adding, renaming, or changing the signature of any public function, or `tests/test_api.py` will fail)
17
+ - **Generate API index locally:** `cd docs && python make_api.py` (writes `docs/api.json`, `docs/llms.txt`, `docs/llms-full.txt`; these are gitignored, and are built automatically with the docs and published at e.g. https://docs.sciris.org/llms.txt)
18
18
  - **Standard import:** `import sciris as sc`
19
19
 
20
20
  ### Core Functions
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sciris
3
- Version: 3.4.0
3
+ Version: 3.5.0
4
4
  Summary: Fast, flexible tools to simplify scientific Python
5
5
  Author-email: Sciris Development Team <info@sciris.org>
6
6
  License-Expression: MIT
@@ -14,6 +14,11 @@ objects.inv
14
14
  # But keep the index.qmd file
15
15
  !/api/index.qmd
16
16
 
17
+ # Ignore the auto-generated API index (see make_api.py)
18
+ /api.json
19
+ /llms.txt
20
+ /llms-full.txt
21
+
17
22
  **/*.quarto_ipynb
18
23
 
19
24
  /.quarto/
@@ -44,4 +44,4 @@ The entire Sciris API — every public function and class, with its signature, s
44
44
  - [llms-full.txt](llms-full.txt): the same, plus a usage example for each function (~90 kB)
45
45
  - [api.json](api.json): the same data as structured JSON, for programmatic use
46
46
 
47
- All three files are generated from the source (`cd docs && python make_api.py`) and are verified in CI, so they never drift from the current version of Sciris.
47
+ All three files are generated from the source each time the docs are built, so they never drift from the current version of Sciris. To generate them locally, run `cd docs && python make_api.py`.
@@ -13,22 +13,18 @@ published with the docs (e.g. <https://docs.sciris.org/llms.txt>):
13
13
 
14
14
  This is a docs build tool: nothing here is part of the Sciris package itself.
15
15
 
16
- To regenerate the artifacts::
16
+ The artifacts are not committed to the repository: they are generated when the
17
+ docs are built. To generate them manually::
17
18
 
18
19
  cd docs && python make_api.py
19
-
20
- To check that they are up to date (used by the test suite, hence CI)::
21
-
22
- cd docs && python make_api.py --check
23
20
  """
24
21
 
25
22
  import re
26
- import sys
27
23
  import inspect
28
24
  import textwrap
29
25
  import sciris as sc
30
26
 
31
- __all__ = ['make_index', 'load', 'write', 'check']
27
+ __all__ = ['make_index', 'load', 'write']
32
28
 
33
29
  thisdir = sc.thispath(__file__)
34
30
  jsonfile = thisdir / 'api.json'
@@ -314,44 +310,5 @@ def write(verbose=True):
314
310
  return written
315
311
 
316
312
 
317
- def check(verbose=True):
318
- """
319
- Check whether the generated artifacts match the current API.
320
-
321
- Args:
322
- verbose (bool): whether to print which files are out of date
323
-
324
- Returns:
325
- The list of files that are out of date (empty if everything matches).
326
- """
327
- index = make_index()
328
- stale = []
329
-
330
- def compare(path, expected):
331
- """ Compare a file to what it should contain, treating a missing file as stale """
332
- if not path.exists():
333
- return 'missing'
334
- actual = sc.loadjson(path) if path.suffix == '.json' else sc.loadtext(path)
335
- return None if actual == expected else 'out of date'
336
-
337
- for path,expected in [(jsonfile, index),
338
- (llmsfile, make_llms_txt(index, examples=False)),
339
- (llmsfullfile, make_llms_txt(index, examples=True))]:
340
- reason = compare(path, expected)
341
- if reason:
342
- stale.append(path)
343
- if verbose:
344
- print(f' {path.name} is {reason}')
345
- if verbose and not stale:
346
- print(f'All API artifacts are up to date ({index["n_entries"]} entries, v{index["version"]})')
347
- return stale
348
-
349
-
350
313
  if __name__ == '__main__':
351
- if '--check' in sys.argv:
352
- stale = check()
353
- if stale:
354
- print('Run "python make_api.py" to regenerate.')
355
- sys.exit(1)
356
- else:
357
- write()
314
+ write()
@@ -101,7 +101,19 @@ print(f'Checking {ob.key1 = }')
101
101
  print(f'Checking {ob["key1"] = }') # We need to use double-quotes inside since single quotes are taken!
102
102
  ```
103
103
 
104
- In most cases, you probably want to use `objdict`s rather than `odict`s just to have the extra flexibility. Why would you ever use an `odict` over an `objdict`? Mostly just because there's small but nonzero overhead in doing the extra attribute checking: `odict` is faster (faster than even `collections.OrderedDict`, though slower than a plain `dict`). The differences are tiny (literally nanoseconds) so won't matter unless you're doing millions of operations. But if you're reading this, chances are high that you _do_ sometimes need to do millions of dict operations.
104
+ In most cases, you probably want to use `objdict`s rather than `odict`s just to have the extra flexibility. Since they're so common, `sc.obj()` is a shorter name for `sc.objdict()`.
105
+
106
+ ### Performance
107
+
108
+ All this flexibility has a cost, although a small one. If you only need attribute access and not the other `odict` features (such as indexing by position), `sc.dictobj()` is a lighter-weight alternative that is nearly as fast as a plain `dict`. Approximate times per operation, in nanoseconds:
109
+
110
+ | | `obj['a']` | `obj.a` | `obj['a'] = 1` | `obj.a = 1` | Create |
111
+ |---|---|---|---|---|---|
112
+ | `dict` | 10 | n/a | 15 | n/a | 50 |
113
+ | `sc.dictobj()` | 20 | 15 | 15 | 20 | 450 |
114
+ | `sc.objdict()` | 40 | 50 | 190 | 330 | 600 |
115
+
116
+ These differences won't matter unless you're doing millions of operations. But if you're reading this, chances are high that you _do_ sometimes need to do millions of dict operations, in which case use `sc.dictobj()` or a plain `dict` inside the loop. The other difference is that an `sc.objdict()` protects its methods (`obj.keys = 3` raises an error, and `obj['keys'] = 3` leaves `obj.keys()` working), whereas in an `sc.dictobj()` a key replaces a method of the same name.
105
117
 
106
118
  ## Dataframes
107
119
 
@@ -0,0 +1,89 @@
1
+ # `sc.objdict` attribute access is slow
2
+
3
+ ## Summary
4
+
5
+ Reading a key from an `sc.objdict` as an attribute (`obj.a`) takes about 530 ns, roughly 35 times slower than `types.SimpleNamespace` or `sc.dictobj` (15 ns) and 6 times slower than reading the same key as an item (`obj['a']`, 90 ns). Writing via an attribute (`obj.a = 1`) takes about 1150 ns, roughly 60 times slower than `SimpleNamespace`. This is invisible in normal use, but it adds up when an `objdict` is read inside a hot loop.
6
+
7
+ ## How it was found
8
+
9
+ In the Starsim v4 ODE solver (`starsim/solvers.py` on the `v4` branch), the right-hand side function read its per-compartment and per-flow records from `objdict`s. Profiling showed attribute access on those objects was about a third of the right-hand side's run time. Swapping the records read in the hot loop for `types.SimpleNamespace` removed the cost. The solver still uses `objdict` everywhere outside the hot loop.
10
+
11
+ ## Measurements
12
+
13
+ Sciris 3.4.0, Python 3.13, Linux; best of 5 runs of 1,000,000 operations each.
14
+
15
+ | Object | `obj.a` | `obj['a']` | `obj.a = 1.0` |
16
+ |---|---|---|---|
17
+ | `dict` | n/a | 10 ns | n/a |
18
+ | `types.SimpleNamespace` | 15 ns | n/a | 19 ns |
19
+ | `sc.odict` | n/a | 65 ns | n/a |
20
+ | `sc.dictobj` | 15 ns | 81 ns | 20 ns |
21
+ | `sc.objdict` | 531 ns | 91 ns | 1148 ns |
22
+
23
+ ```python
24
+ import timeit
25
+ import types
26
+ import sciris as sc
27
+
28
+ n = 1_000_000
29
+ data = dict(a=1.0, b=2.0, c=3.0)
30
+ objs = dict(
31
+ SimpleNamespace = types.SimpleNamespace(**data),
32
+ dictobj = sc.dictobj(data),
33
+ objdict = sc.objdict(data),
34
+ )
35
+ for name,obj in objs.items():
36
+ for stmt in ['obj.a', 'obj.a = 1.0']:
37
+ t = min(timeit.repeat(stmt, globals=dict(obj=obj), number=n, repeat=5))/n*1e9
38
+ print(f'{name:16s} {stmt:12s} {t:6.0f} ns')
39
+ ```
40
+
41
+ ## Cause
42
+
43
+ `objdict.__getattribute__` (in `sciris/sc_odict.py`) tries the name as a real attribute first and falls back to the dict only if that fails:
44
+
45
+ ```python
46
+ try: # First, try to get the attribute as an attribute
47
+ return object.__getattribute__(self, attr)
48
+ except Exception as E: # If that fails, try to get it as a dict item, but pass along the original exception
49
+ return self.__getitem__(attr, exception=E)
50
+ ```
51
+
52
+ So every successful read of a key raises and catches an `AttributeError` before reaching the dict, and then goes through two more Python-level calls (`objdict.__getitem__`, then `odict.__getitem__`). Creating and catching the exception is most of the cost.
53
+
54
+ `objdict.__setattr__` does the same thing: it calls `object.__getattribute__` to check that the name is not an existing attribute, which raises for every ordinary key, and then calls `__setitem__`.
55
+
56
+ Reading a method or other real attribute (`obj.keys`) does not hit the exception and takes about 90 ns.
57
+
58
+ ## Possible fix
59
+
60
+ Check the dict first, and use it directly when the name is a key and is not also an attribute of the class. Methods still win over keys of the same name, so behavior is unchanged; everything else falls through to the existing code.
61
+
62
+ ```python
63
+ def __getattribute__(self, attr):
64
+ if dict.__contains__(self, attr) and not hasattr(type(self), attr): # A key that doesn't shadow a method
65
+ return dict.__getitem__(self, attr)
66
+ ... # Existing code
67
+
68
+ def __setattr__(self, name, value):
69
+ if dict.__contains__(self, name) and not hasattr(type(self), name): # An existing key
70
+ return dict.__setitem__(self, name, value)
71
+ ... # Existing code
72
+ ```
73
+
74
+ Timings for a prototype of this, written as a subclass of `sc.objdict`:
75
+
76
+ | Operation | Current | Prototype |
77
+ |---|---|---|
78
+ | `obj.a` (existing key) | 532 ns | 62 ns |
79
+ | `obj.a = 1.0` (existing key) | 1178 ns | 101 ns |
80
+ | `obj.keys` (method) | 89 ns | 140 ns |
81
+ | `obj.new = 1.0; del obj['new']` (new key) | 1493 ns | 1819 ns |
82
+
83
+ Reading and writing existing keys become about 9 and 12 times faster. Method lookups and setting new keys become slower (by about 50 ns and 330 ns), because they now pay for the extra check before falling through; some of that is the overhead of the subclass calling the parent method, which a change made in `objdict` itself would not have.
84
+
85
+ The prototype passed a few quick checks (a key named `keys` does not shadow the method, integer indexing still works, `defaultdict='nested'` still works), but it has not been run against the Sciris test suite, and nothing in Sciris has been changed.
86
+
87
+ ## Workaround
88
+
89
+ In a hot loop, read `objdict` keys as items (`obj['a']`), or hold the records in a `types.SimpleNamespace` or `sc.dictobj` instead.
@@ -402,7 +402,7 @@ class IterObj:
402
402
  # Attributes with initialization required
403
403
  self._trace = []
404
404
  self._memo = co.defaultdict(int)
405
- self.output = sc.objdict()
405
+ self.output = {} # Converted to an objdict once the iteration is complete, since it's much faster to populate a dict
406
406
  if self.func is None: # If no function provided, define a function that just returns the contents of the current node
407
407
  self.func = lambda obj: obj
408
408
 
@@ -620,6 +620,7 @@ class IterObj:
620
620
  return self.func(self.obj, **self.func_kw) # Set at the root level
621
621
  return self.obj
622
622
  else:
623
+ self.output = sc.objdict(self.output)
623
624
  return self.output
624
625
 
625
626
  def flatten_traces(self, sep='_', inplace=True):
@@ -8,18 +8,19 @@ Highlights:
8
8
  import re
9
9
  import sys
10
10
  import json
11
+ import copyreg
11
12
  import numpy as np
12
13
  import collections as co
13
14
  import sciris as sc
14
15
  from .sc_utils import _not_given # Imported directly since it's needed at definition time
15
16
 
16
17
  # Restrict imports to user-facing modules
17
- __all__ = ['ddict', 'counter', 'odict', 'objdict', 'dictobj', 'asobj', 'argparse']
18
+ __all__ = ['ddict', 'counter', 'odict', 'objdict', 'obj', 'dictobj', 'asobj', 'argparse']
18
19
 
19
20
  ddict = co.defaultdict # Define alias
20
21
 
21
22
  # Critical attributes that should be resolved immediately, not via any special handling
22
- _fundamental_attrs = ['__class__', '__reduce__', '__reduce_ex__', '__getstate__', '__setstate__', '__copy__','__deepcopy__']
23
+ _fundamental_attrs = frozenset(['__class__', '__reduce__', '__reduce_ex__', '__getstate__', '__setstate__', '__copy__','__deepcopy__'])
23
24
 
24
25
 
25
26
  class counter(co.Counter):
@@ -140,6 +141,8 @@ class odict(dict):
140
141
  - *New in version 3.1.1:* allow steps in slices; `copy()` now behaves as a standard dict
141
142
  """
142
143
 
144
+ _stale = True # The keys are only cached (as a list, for integer lookup) when first needed
145
+
143
146
  def __init__(self, *args, defaultdict=None, **kwargs):
144
147
  """ Combine the init properties of both OrderedDict and defaultdict """
145
148
 
@@ -154,7 +157,6 @@ class odict(dict):
154
157
  raise TypeError(errormsg)
155
158
  self._setattr('_defaultdict', defaultdict) # Use object.__setattr__() since setattr() is overridden by sc.objdict()
156
159
 
157
- self._cache_keys()
158
160
  return
159
161
 
160
162
 
@@ -240,10 +242,10 @@ class odict(dict):
240
242
  def __setitem__(self, key, value):
241
243
  """ Allows setitem to support strings, integers, slices, lists, or arrays """
242
244
 
243
- self._setattr('_stale', True) # Flag to refresh the cached keys
245
+ object.__setattr__(self, '_stale', True) # Flag to refresh the cached keys
244
246
 
245
247
  if isinstance(key, (str,tuple)):
246
- self._setitem(key, value)
248
+ dict.__setitem__(self, key, value)
247
249
 
248
250
  elif isinstance(key, sc._numtype): # Convert automatically from float...dangerous?
249
251
  thiskey = key if dict.__contains__(self, key) else self._ikey(key) # As with getitem, an existing key takes precedence
@@ -1260,6 +1262,8 @@ class objdict(odict):
1260
1262
  instead act on dict keys (e.g. `obj['x'] = 3`). If you want to actually get/set
1261
1263
  an attribute, use `obj.getattribute()`/`obj.setattribute()`.
1262
1264
 
1265
+ `sc.obj()` is an alias for `sc.objdict()`.
1266
+
1263
1267
  For a lighter-weight example (an object that acts like a dict), see `sc.dictobj()`.
1264
1268
 
1265
1269
  **Examples**:
@@ -1281,6 +1285,8 @@ class objdict(odict):
1281
1285
 
1282
1286
  For a lighter-weight equivalent (based on `dict` instead of `odict`), see
1283
1287
  `sc.dictobj()`.
1288
+
1289
+ - *New in version 3.5.0:* `sc.obj()` alias; much faster attribute access
1284
1290
  """
1285
1291
 
1286
1292
  def __init__(self, *args, **kwargs):
@@ -1298,40 +1304,41 @@ class objdict(odict):
1298
1304
  return odict.__repr__(self, quote='', numsep='.', classname=self.__class__.__name__, *args, **kwargs)
1299
1305
 
1300
1306
 
1301
- def __getattribute__(self, attr):
1302
- """ Handle as attributes first, then as dict keys """
1303
- # Pickle/copy protocol: must resolve from class, not as dict keys
1307
+ def __getattr__(self, attr):
1308
+ """ Only called if not found as an attribute, so get it as a dict key """
1309
+ if attr not in _fundamental_attrs: # Pickle/copy protocol: must resolve from class, not as dict keys
1310
+ try: # The usual case: it's a key
1311
+ return dict.__getitem__(self, attr)
1312
+ except KeyError:
1313
+ pass
1314
+ errormsg = f"'{type(self).__name__}' object has no attribute '{attr}'"
1315
+ exception = AttributeError(errormsg, name=attr, obj=self)
1304
1316
  if attr in _fundamental_attrs:
1305
- return object.__getattribute__(self, attr)
1306
- else:
1307
- try: # First, try to get the attribute as an attribute
1308
- return object.__getattribute__(self, attr)
1309
- except Exception as E: # If that fails, try to get it as a dict item, but pass along the original exception
1310
- return self.__getitem__(attr, exception=E)
1317
+ raise exception
1318
+ return self.__getitem__(attr, exception=exception) # Handle defaults, or else raise the exception
1311
1319
 
1312
1320
 
1313
1321
  def __setattr__(self, name, value):
1314
1322
  """ Set key in dict, not attribute! """
1315
1323
  if name[:2] == '__': # If it starts with a double underscore, it's almost certainly an attribute, not a key
1316
1324
  return object.__setattr__(self, name, value)
1325
+ elif name in self.__dict__ or hasattr(type(self), name):
1326
+ errormsg = f'"{name}" exists as an attribute, so cannot be set as key; use setattribute() instead'
1327
+ raise KeyError(errormsg)
1317
1328
  else:
1318
- try:
1319
- object.__getattribute__(self, name) # Try retrieving this as an attribute, expect AttributeError...
1320
- errormsg = f'"{name}" exists as an attribute, so cannot be set as key; use setattribute() instead'
1321
- raise KeyError(errormsg)
1322
- except AttributeError:
1323
- return self.__setitem__(name, value) # If so, simply return
1329
+ return self.__setitem__(name, value)
1324
1330
 
1325
1331
 
1326
1332
  def __getitem__(self, attr, exception=None):
1327
1333
  """ Handle dict keys, including nested defaultdict logic """
1328
1334
  try: # Try retrieving normally
1335
+ return dict.__getitem__(self, attr)
1336
+ except Exception:
1337
+ pass
1338
+ try: # Then try integers, slices, etc.
1329
1339
  return odict.__getitem__(self, attr, allow_default=False) # Do not allow odict to handle default
1330
1340
  except Exception as E2: # If that fails, raise the original exception
1331
- try: # Handle defaultdict behavior by first checking if it exists
1332
- _defaultdict = object.__getattribute__(self, '_defaultdict')
1333
- except:
1334
- _defaultdict = None
1341
+ _defaultdict = self.__dict__.get('_defaultdict') # Handle defaultdict behavior by first checking if it exists
1335
1342
  if _defaultdict is not None: # If it does, use it, then get the key again
1336
1343
  if _defaultdict == 'nested':
1337
1344
  return self._new(defaultdict='nested', _nested_parent=self, _nested_attr=attr) # Create a recursive object with links back to the ultimate parent
@@ -1349,14 +1356,9 @@ class objdict(odict):
1349
1356
  def __setitem__(self, name, value):
1350
1357
  """ Set as a dict item """
1351
1358
  odict.__setitem__(self, name, value)
1352
- try:
1353
- p = object.__getattribute__(self, '_nested_parent')
1354
- key = object.__getattribute__(self, '_nested_attr')
1355
- except AttributeError:
1356
- p = None
1357
- key = None
1358
- if p is not None:
1359
- p[key] = self
1359
+ attrs = self.__dict__
1360
+ if '_nested_parent' in attrs:
1361
+ attrs['_nested_parent'][attrs['_nested_attr']] = self
1360
1362
  object.__delattr__(self, '_nested_parent')
1361
1363
  object.__delattr__(self, '_nested_attr')
1362
1364
  return
@@ -1389,6 +1391,9 @@ class objdict(odict):
1389
1391
  return object.__delattr__(self, name)
1390
1392
 
1391
1393
 
1394
+ obj = objdict # Define alias
1395
+
1396
+
1392
1397
  class dictobj(dict):
1393
1398
  """
1394
1399
  Lightweight class to create an object that can also act like a dictionary.
@@ -1403,35 +1408,46 @@ class dictobj(dict):
1403
1408
  ```
1404
1409
  For a more powerful alternative, see `sc.objdict()`.
1405
1410
 
1406
- **Note**: because `dictobj` is halfway between a dict and an object, it can't
1407
- be automatically converted to a JSON (but will fail silently). Use `to_json()`
1408
- instead.
1411
+ The keys and the attributes are the same thing (the dict is its own `__dict__`),
1412
+ so it is as fast as a plain object for attributes and nearly as fast as a plain
1413
+ dict for items. This also means that keys can overwrite methods (e.g. `obj.items = 5`).
1409
1414
 
1410
1415
  - *New in version 1.3.0.*
1411
1416
  - *New in version 1.3.1:* inherit from dict
1412
1417
  - *New in version 2.0.0:* allow positional arguments
1413
1418
  - *New in version 3.0.0:* "fromkeys" now a class method; `to_json()` method
1414
1419
  - *New in version 3.1.6:* "copy" returns another dictobj
1420
+ - *New in version 3.5.0:* keys are stored in the dict itself, so e.g. `json.dumps()` works; faster
1415
1421
  """
1416
1422
 
1423
+ def __new__(cls, *args, **kwargs):
1424
+ self = dict.__new__(cls)
1425
+ self.__dict__ = self # Use the dict itself to store the attributes
1426
+ return self
1427
+
1417
1428
  def __init__(self, *args, **kwargs):
1418
- kwargs = sc.mergedicts(*args, kwargs)
1419
- for k,v in kwargs.items():
1420
- self.__dict__[k] = v
1429
+ for arg in args:
1430
+ if arg is not None:
1431
+ self.update(arg)
1432
+ self.update(kwargs)
1421
1433
  return
1422
1434
 
1423
1435
  def __repr__(self):
1424
- output = 'dictobj(' + self.__dict__.__repr__() + ')'
1436
+ output = 'dictobj(' + dict.__repr__(self) + ')'
1425
1437
  return output
1426
1438
 
1427
- def to_json(self):
1428
- """ Export the dictobj to JSON (NB: regular `json.dumps()` does not work) """
1429
- return json.dumps(self.__dict__)
1439
+ def __reduce__(self):
1440
+ """ Pickle and copy the contents once, rather than as both items and attributes """
1441
+ return (copyreg.__newobj__, (self.__class__,), dict(self))
1430
1442
 
1431
- @classmethod
1432
- def fromkeys(cls, *args, **kwargs):
1433
- """ Create a new dictobj from keys """
1434
- return cls(dict.fromkeys(*args, **kwargs))
1443
+ def __setstate__(self, state):
1444
+ """ Restore from a pickle or copy, including pickles from before version 3.5.0 """
1445
+ self.update(state)
1446
+ return
1447
+
1448
+ def to_json(self):
1449
+ """ Export the dictobj to JSON """
1450
+ return json.dumps(self)
1435
1451
 
1436
1452
  def copy(self):
1437
1453
  """ Create a shallow copy """
@@ -1439,37 +1455,15 @@ class dictobj(dict):
1439
1455
  new.update(self)
1440
1456
  return new
1441
1457
 
1442
- # Copy other default dictionary methods
1443
- def __getitem__( self, *args, **kwargs): return self.__dict__.__getitem__( *args, **kwargs)
1444
- def __setitem__( self, *args, **kwargs): return self.__dict__.__setitem__( *args, **kwargs)
1445
- def __contains__(self, *args, **kwargs): return self.__dict__.__contains__(*args, **kwargs)
1446
- def __len__( self, *args, **kwargs): return self.__dict__.__len__( *args, **kwargs)
1447
- def __iter__( self, *args, **kwargs): return self.__dict__.__iter__( *args, **kwargs)
1448
- def clear( self, *args, **kwargs): return self.__dict__.clear( *args, **kwargs)
1449
- def get( self, *args, **kwargs): return self.__dict__.get( *args, **kwargs)
1450
- def items( self, *args, **kwargs): return self.__dict__.items( *args, **kwargs)
1451
- def keys( self, *args, **kwargs): return self.__dict__.keys( *args, **kwargs)
1452
- def pop( self, *args, **kwargs): return self.__dict__.pop( *args, **kwargs)
1453
- def popitem( self, *args, **kwargs): return self.__dict__.popitem( *args, **kwargs)
1454
- def setdefault( self, *args, **kwargs): return self.__dict__.setdefault( *args, **kwargs)
1455
- def update( self, *args, **kwargs): return self.__dict__.update( *args, **kwargs)
1456
- def values( self, *args, **kwargs): return self.__dict__.values( *args, **kwargs)
1457
- def __reversed__(self, *args, **kwargs): return self.__dict__.__reversed__(*args, **kwargs)
1458
-
1459
- # The dict storage itself is empty, so comparisons and merges must also use __dict__
1460
- def __eq__(self, other): return self.__dict__ == (other.__dict__ if isinstance(other, dictobj) else other)
1461
- def __ne__(self, other): return not self.__eq__(other)
1462
1458
  def __or__(self, other):
1463
1459
  new = self.copy()
1464
1460
  new.update(other)
1465
1461
  return new
1462
+
1466
1463
  def __ror__(self, other):
1467
1464
  new = self.__class__(other)
1468
1465
  new.update(self)
1469
1466
  return new
1470
- def __ior__(self, other):
1471
- self.update(other)
1472
- return self
1473
1467
 
1474
1468
 
1475
1469
  def asobj(obj, strict=True):
@@ -4,6 +4,6 @@ Version and license information.
4
4
 
5
5
  __all__ = ['__version__', '__versiondate__', '__license__']
6
6
 
7
- __version__ = '3.4.0'
8
- __versiondate__ = '2026-09-28'
7
+ __version__ = '3.5.0'
8
+ __versiondate__ = '2026-10-13'
9
9
  __license__ = f'Sciris {__version__} ({__versiondate__}) – © 2014-2026 by the Sciris Development Team'
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sciris
3
- Version: 3.4.0
3
+ Version: 3.5.0
4
4
  Summary: Fast, flexible tools to simplify scientific Python
5
5
  Author-email: Sciris Development Team <info@sciris.org>
6
6
  License-Expression: MIT
@@ -9,6 +9,7 @@ MANIFEST.in
9
9
  README.md
10
10
  STYLE_GUIDE.md
11
11
  context7.json
12
+ objdict_performance.md
12
13
  pyproject.toml
13
14
  .claude-plugin/marketplace.json
14
15
  .github/workflows/publish_docs.yaml
@@ -36,15 +37,12 @@ docs/CNAME
36
37
  docs/README.md
37
38
  docs/_quarto.yml
38
39
  docs/_variables.yml
39
- docs/api.json
40
40
  docs/check_notebooks.py
41
41
  docs/clean_all
42
42
  docs/clean_outputs.py
43
43
  docs/conduct.qmd
44
44
  docs/contributing.qmd
45
45
  docs/index.qmd
46
- docs/llms-full.txt
47
- docs/llms.txt
48
46
  docs/make_api.py
49
47
  docs/overview.qmd
50
48
  docs/preview
@@ -1,5 +1,5 @@
1
1
  '''
2
- Test the machine-readable API index (docs/api.json, docs/llms.txt), which is generated by docs/make_api.py.
2
+ Test the machine-readable API index (api.json, llms.txt), which is generated by docs/make_api.py when the docs are built.
3
3
  '''
4
4
 
5
5
  import sys
@@ -67,26 +67,11 @@ def test_llms_txt():
67
67
  return full
68
68
 
69
69
 
70
- def test_generated_index():
71
- sc.heading('Test the generated API index')
72
-
73
- index = make_api.load()
74
- assert index['version'] == sc.__version__
75
- assert index['n_entries'] == len(index['entries'])
76
-
77
- print('Checking that the generated artifacts are up to date')
78
- stale = make_api.check()
79
- assert not stale, f'API artifacts are out of date: {stale}. Run "cd docs && python make_api.py" to regenerate.'
80
-
81
- return index
82
-
83
-
84
70
  #%% Run as a script
85
71
  if __name__ == '__main__':
86
72
  T = sc.timer()
87
73
 
88
74
  index = test_make_index()
89
75
  text = test_llms_txt()
90
- generated = test_generated_index()
91
76
 
92
77
  T.toc('Done.')
@@ -295,6 +295,8 @@ def test_asobj():
295
295
  with pytest.raises(AttributeError):
296
296
  myobjdict.setattribute('keys', 4)
297
297
 
298
+ assert sc.obj is sc.objdict # Alias
299
+
298
300
  print('Testing dictobj')
299
301
  obj = sc.dictobj(x=4, y=6)
300
302
  obj.a = 5
@@ -305,6 +307,11 @@ def test_asobj():
305
307
  assert 'x' in json
306
308
  assert fk.foo is None
307
309
  assert sc.dictobj(x=1) != sc.dictobj(x=2) and sc.dictobj(x=1) == {'x':1} # Compares contents
310
+ assert sc.loadjson(string=json) == obj == sc.dcp(obj) == sc.loadstr(sc.dumpstr(obj)) # Keys and attributes are the same thing
311
+ assert isinstance(obj | {'c':1}, sc.dictobj) and isinstance(obj.copy(), sc.dictobj)
312
+ old = sc.dictobj.__new__(sc.dictobj)
313
+ old.__setstate__({'x':4}) # Pickles from before 3.5.0 stored the data as attributes
314
+ assert old.x == old['x'] == 4
308
315
 
309
316
  return
310
317