sciris 3.5.2__tar.gz → 3.6.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 (169) hide show
  1. {sciris-3.5.2 → sciris-3.6.0}/CHANGELOG.md +9 -0
  2. {sciris-3.5.2/sciris.egg-info → sciris-3.6.0}/PKG-INFO +1 -1
  3. {sciris-3.5.2 → sciris-3.6.0}/sciris/sc_odict.py +147 -44
  4. {sciris-3.5.2 → sciris-3.6.0}/sciris/sc_version.py +2 -2
  5. {sciris-3.5.2 → sciris-3.6.0/sciris.egg-info}/PKG-INFO +1 -1
  6. {sciris-3.5.2 → sciris-3.6.0}/tests/test_odict.py +14 -0
  7. {sciris-3.5.2 → sciris-3.6.0}/.claude-plugin/marketplace.json +0 -0
  8. {sciris-3.5.2 → sciris-3.6.0}/.github/workflows/publish_docs.yaml +0 -0
  9. {sciris-3.5.2 → sciris-3.6.0}/.github/workflows/pypi_release.yaml +0 -0
  10. {sciris-3.5.2 → sciris-3.6.0}/.github/workflows/schedule_tests.yaml +0 -0
  11. {sciris-3.5.2 → sciris-3.6.0}/.github/workflows/test_docs.yaml +0 -0
  12. {sciris-3.5.2 → sciris-3.6.0}/.github/workflows/test_downstream.yaml +0 -0
  13. {sciris-3.5.2 → sciris-3.6.0}/.github/workflows/test_sciris.yaml +0 -0
  14. {sciris-3.5.2 → sciris-3.6.0}/.gitignore +0 -0
  15. {sciris-3.5.2 → sciris-3.6.0}/.pylintrc +0 -0
  16. {sciris-3.5.2 → sciris-3.6.0}/.pytest_cache/README.md +0 -0
  17. {sciris-3.5.2 → sciris-3.6.0}/CLAUDE.md +0 -0
  18. {sciris-3.5.2 → sciris-3.6.0}/CODE_OF_CONDUCT.md +0 -0
  19. {sciris-3.5.2 → sciris-3.6.0}/CONTRIBUTING.md +0 -0
  20. {sciris-3.5.2 → sciris-3.6.0}/LICENSE +0 -0
  21. {sciris-3.5.2 → sciris-3.6.0}/MANIFEST.in +0 -0
  22. {sciris-3.5.2 → sciris-3.6.0}/README.md +0 -0
  23. {sciris-3.5.2 → sciris-3.6.0}/STYLE_GUIDE.md +0 -0
  24. {sciris-3.5.2 → sciris-3.6.0}/claude_plugin/.claude-plugin/plugin.json +0 -0
  25. {sciris-3.5.2 → sciris-3.6.0}/claude_plugin/README.md +0 -0
  26. {sciris-3.5.2 → sciris-3.6.0}/claude_plugin/skills/sciris-advanced/SKILL.md +0 -0
  27. {sciris-3.5.2 → sciris-3.6.0}/claude_plugin/skills/sciris-arrays/SKILL.md +0 -0
  28. {sciris-3.5.2 → sciris-3.6.0}/claude_plugin/skills/sciris-dates/SKILL.md +0 -0
  29. {sciris-3.5.2 → sciris-3.6.0}/claude_plugin/skills/sciris-dicts/SKILL.md +0 -0
  30. {sciris-3.5.2 → sciris-3.6.0}/claude_plugin/skills/sciris-files/SKILL.md +0 -0
  31. {sciris-3.5.2 → sciris-3.6.0}/claude_plugin/skills/sciris-intro/SKILL.md +0 -0
  32. {sciris-3.5.2 → sciris-3.6.0}/claude_plugin/skills/sciris-parallel/SKILL.md +0 -0
  33. {sciris-3.5.2 → sciris-3.6.0}/claude_plugin/skills/sciris-plotting/SKILL.md +0 -0
  34. {sciris-3.5.2 → sciris-3.6.0}/claude_plugin/skills/sciris-printing/SKILL.md +0 -0
  35. {sciris-3.5.2 → sciris-3.6.0}/claude_plugin/skills/sciris-utils/SKILL.md +0 -0
  36. {sciris-3.5.2 → sciris-3.6.0}/context7.json +0 -0
  37. {sciris-3.5.2 → sciris-3.6.0}/docs/.gitignore +0 -0
  38. {sciris-3.5.2 → sciris-3.6.0}/docs/404.qmd +0 -0
  39. {sciris-3.5.2 → sciris-3.6.0}/docs/CNAME +0 -0
  40. {sciris-3.5.2 → sciris-3.6.0}/docs/README.md +0 -0
  41. {sciris-3.5.2 → sciris-3.6.0}/docs/_extensions/machow/interlinks/objects.txt +0 -0
  42. {sciris-3.5.2 → sciris-3.6.0}/docs/_quarto.yml +0 -0
  43. {sciris-3.5.2 → sciris-3.6.0}/docs/_site/llms-full.txt +0 -0
  44. {sciris-3.5.2 → sciris-3.6.0}/docs/_site/llms.txt +0 -0
  45. {sciris-3.5.2 → sciris-3.6.0}/docs/_site/robots.txt +0 -0
  46. {sciris-3.5.2 → sciris-3.6.0}/docs/_variables.yml +0 -0
  47. {sciris-3.5.2 → sciris-3.6.0}/docs/api/index.qmd +0 -0
  48. {sciris-3.5.2 → sciris-3.6.0}/docs/assets/favicon.ico +0 -0
  49. {sciris-3.5.2 → sciris-3.6.0}/docs/assets/sciris-logo-dark.png +0 -0
  50. {sciris-3.5.2 → sciris-3.6.0}/docs/assets/sciris-logo-grey.png +0 -0
  51. {sciris-3.5.2 → sciris-3.6.0}/docs/assets/sciris-logo-large.png +0 -0
  52. {sciris-3.5.2 → sciris-3.6.0}/docs/assets/sciris-logo.png +0 -0
  53. {sciris-3.5.2 → sciris-3.6.0}/docs/assets/sciris-showcase-code.png +0 -0
  54. {sciris-3.5.2 → sciris-3.6.0}/docs/assets/sciris-showcase-output.png +0 -0
  55. {sciris-3.5.2 → sciris-3.6.0}/docs/assets/styles-dark.scss +0 -0
  56. {sciris-3.5.2 → sciris-3.6.0}/docs/assets/styles-light.scss +0 -0
  57. {sciris-3.5.2 → sciris-3.6.0}/docs/assets/styles.css +0 -0
  58. {sciris-3.5.2 → sciris-3.6.0}/docs/check_notebooks.py +0 -0
  59. {sciris-3.5.2 → sciris-3.6.0}/docs/clean_all +0 -0
  60. {sciris-3.5.2 → sciris-3.6.0}/docs/clean_outputs.py +0 -0
  61. {sciris-3.5.2 → sciris-3.6.0}/docs/conduct.qmd +0 -0
  62. {sciris-3.5.2 → sciris-3.6.0}/docs/contributing.qmd +0 -0
  63. {sciris-3.5.2 → sciris-3.6.0}/docs/index.qmd +0 -0
  64. {sciris-3.5.2 → sciris-3.6.0}/docs/make_api.py +0 -0
  65. {sciris-3.5.2 → sciris-3.6.0}/docs/overview.qmd +0 -0
  66. {sciris-3.5.2 → sciris-3.6.0}/docs/preview +0 -0
  67. {sciris-3.5.2 → sciris-3.6.0}/docs/publish +0 -0
  68. {sciris-3.5.2 → sciris-3.6.0}/docs/quarto_utils.py +0 -0
  69. {sciris-3.5.2 → sciris-3.6.0}/docs/render +0 -0
  70. {sciris-3.5.2 → sciris-3.6.0}/docs/requirements.txt +0 -0
  71. {sciris-3.5.2 → sciris-3.6.0}/docs/sciris-citation.bib +0 -0
  72. {sciris-3.5.2 → sciris-3.6.0}/docs/style_guide.qmd +0 -0
  73. {sciris-3.5.2 → sciris-3.6.0}/docs/tutorials/.jupyter_cache/__version__.txt +0 -0
  74. {sciris-3.5.2 → sciris-3.6.0}/docs/tutorials/README.md +0 -0
  75. {sciris-3.5.2 → sciris-3.6.0}/docs/tutorials/convert_notebooks +0 -0
  76. {sciris-3.5.2 → sciris-3.6.0}/docs/tutorials/dancing_lines.gif +0 -0
  77. {sciris-3.5.2 → sciris-3.6.0}/docs/tutorials/img-advanced.gif +0 -0
  78. {sciris-3.5.2 → sciris-3.6.0}/docs/tutorials/img-arrays.png +0 -0
  79. {sciris-3.5.2 → sciris-3.6.0}/docs/tutorials/img-dates.png +0 -0
  80. {sciris-3.5.2 → sciris-3.6.0}/docs/tutorials/img-dicts.png +0 -0
  81. {sciris-3.5.2 → sciris-3.6.0}/docs/tutorials/img-files.png +0 -0
  82. {sciris-3.5.2 → sciris-3.6.0}/docs/tutorials/img-intro.png +0 -0
  83. {sciris-3.5.2 → sciris-3.6.0}/docs/tutorials/img-parallel.png +0 -0
  84. {sciris-3.5.2 → sciris-3.6.0}/docs/tutorials/img-plotting.png +0 -0
  85. {sciris-3.5.2 → sciris-3.6.0}/docs/tutorials/img-printing.png +0 -0
  86. {sciris-3.5.2 → sciris-3.6.0}/docs/tutorials/img-utils.png +0 -0
  87. {sciris-3.5.2 → sciris-3.6.0}/docs/tutorials/sim1/sim.py +0 -0
  88. {sciris-3.5.2 → sciris-3.6.0}/docs/tutorials/sim2/sim.py +0 -0
  89. {sciris-3.5.2 → sciris-3.6.0}/docs/tutorials/tut_advanced.qmd +0 -0
  90. {sciris-3.5.2 → sciris-3.6.0}/docs/tutorials/tut_arrays.qmd +0 -0
  91. {sciris-3.5.2 → sciris-3.6.0}/docs/tutorials/tut_dates.qmd +0 -0
  92. {sciris-3.5.2 → sciris-3.6.0}/docs/tutorials/tut_dicts.qmd +0 -0
  93. {sciris-3.5.2 → sciris-3.6.0}/docs/tutorials/tut_files.qmd +0 -0
  94. {sciris-3.5.2 → sciris-3.6.0}/docs/tutorials/tut_intro.qmd +0 -0
  95. {sciris-3.5.2 → sciris-3.6.0}/docs/tutorials/tut_parallel.qmd +0 -0
  96. {sciris-3.5.2 → sciris-3.6.0}/docs/tutorials/tut_plotting.qmd +0 -0
  97. {sciris-3.5.2 → sciris-3.6.0}/docs/tutorials/tut_printing.qmd +0 -0
  98. {sciris-3.5.2 → sciris-3.6.0}/docs/tutorials/tut_utils.qmd +0 -0
  99. {sciris-3.5.2 → sciris-3.6.0}/docs/tutorials.qmd +0 -0
  100. {sciris-3.5.2 → sciris-3.6.0}/docs/whatsnew.qmd +0 -0
  101. {sciris-3.5.2 → sciris-3.6.0}/pyproject.toml +0 -0
  102. {sciris-3.5.2 → sciris-3.6.0}/sciris/README.md +0 -0
  103. {sciris-3.5.2 → sciris-3.6.0}/sciris/__init__.py +0 -0
  104. {sciris-3.5.2 → sciris-3.6.0}/sciris/_extras/README.md +0 -0
  105. {sciris-3.5.2 → sciris-3.6.0}/sciris/_extras/__init__.py +0 -0
  106. {sciris-3.5.2 → sciris-3.6.0}/sciris/_extras/ansicolors.py +0 -0
  107. {sciris-3.5.2 → sciris-3.6.0}/sciris/fancy.mplstyle +0 -0
  108. {sciris-3.5.2 → sciris-3.6.0}/sciris/sc_asd.py +0 -0
  109. {sciris-3.5.2 → sciris-3.6.0}/sciris/sc_colors.py +0 -0
  110. {sciris-3.5.2 → sciris-3.6.0}/sciris/sc_dataframe.py +0 -0
  111. {sciris-3.5.2 → sciris-3.6.0}/sciris/sc_datetime.py +0 -0
  112. {sciris-3.5.2 → sciris-3.6.0}/sciris/sc_fileio.py +0 -0
  113. {sciris-3.5.2 → sciris-3.6.0}/sciris/sc_math.py +0 -0
  114. {sciris-3.5.2 → sciris-3.6.0}/sciris/sc_nested.py +0 -0
  115. {sciris-3.5.2 → sciris-3.6.0}/sciris/sc_parallel.py +0 -0
  116. {sciris-3.5.2 → sciris-3.6.0}/sciris/sc_plotting.py +0 -0
  117. {sciris-3.5.2 → sciris-3.6.0}/sciris/sc_printing.py +0 -0
  118. {sciris-3.5.2 → sciris-3.6.0}/sciris/sc_profiling.py +0 -0
  119. {sciris-3.5.2 → sciris-3.6.0}/sciris/sc_settings.py +0 -0
  120. {sciris-3.5.2 → sciris-3.6.0}/sciris/sc_utils.py +0 -0
  121. {sciris-3.5.2 → sciris-3.6.0}/sciris/sc_versioning.py +0 -0
  122. {sciris-3.5.2 → sciris-3.6.0}/sciris/simple.mplstyle +0 -0
  123. {sciris-3.5.2 → sciris-3.6.0}/sciris.egg-info/SOURCES.txt +0 -0
  124. {sciris-3.5.2 → sciris-3.6.0}/sciris.egg-info/dependency_links.txt +0 -0
  125. {sciris-3.5.2 → sciris-3.6.0}/sciris.egg-info/requires.txt +0 -0
  126. {sciris-3.5.2 → sciris-3.6.0}/sciris.egg-info/top_level.txt +0 -0
  127. {sciris-3.5.2 → sciris-3.6.0}/setup.cfg +0 -0
  128. {sciris-3.5.2 → sciris-3.6.0}/tests/.coveragerc +0 -0
  129. {sciris-3.5.2 → sciris-3.6.0}/tests/.pytest_cache/README.md +0 -0
  130. {sciris-3.5.2 → sciris-3.6.0}/tests/README.rst +0 -0
  131. {sciris-3.5.2 → sciris-3.6.0}/tests/check_coverage +0 -0
  132. {sciris-3.5.2 → sciris-3.6.0}/tests/check_style +0 -0
  133. {sciris-3.5.2 → sciris-3.6.0}/tests/files/archive_2023-04-19.zip +0 -0
  134. {sciris-3.5.2 → sciris-3.6.0}/tests/files/deadclass.obj +0 -0
  135. {sciris-3.5.2 → sciris-3.6.0}/tests/files/deadclass.py +0 -0
  136. {sciris-3.5.2 → sciris-3.6.0}/tests/files/exampledata.xlsx +0 -0
  137. {sciris-3.5.2 → sciris-3.6.0}/tests/files/examplefont.ttf +0 -0
  138. {sciris-3.5.2 → sciris-3.6.0}/tests/files/make_archive.py +0 -0
  139. {sciris-3.5.2 → sciris-3.6.0}/tests/files/make_deadclass.py +0 -0
  140. {sciris-3.5.2 → sciris-3.6.0}/tests/files/pickle_2021-01-01.obj +0 -0
  141. {sciris-3.5.2 → sciris-3.6.0}/tests/files/pickle_2022-01-01.obj +0 -0
  142. {sciris-3.5.2 → sciris-3.6.0}/tests/pytest.ini +0 -0
  143. {sciris-3.5.2 → sciris-3.6.0}/tests/regression/README.rst +0 -0
  144. {sciris-3.5.2 → sciris-3.6.0}/tests/regression/run_downstream +0 -0
  145. {sciris-3.5.2 → sciris-3.6.0}/tests/regression/run_regression +0 -0
  146. {sciris-3.5.2 → sciris-3.6.0}/tests/regression/sciris2021-01-01.yml +0 -0
  147. {sciris-3.5.2 → sciris-3.6.0}/tests/regression/sciris2022-01-01.yml +0 -0
  148. {sciris-3.5.2 → sciris-3.6.0}/tests/regression/sciris2023-01-01.yml +0 -0
  149. {sciris-3.5.2 → sciris-3.6.0}/tests/regression/setup_envs +0 -0
  150. {sciris-3.5.2 → sciris-3.6.0}/tests/requirements.txt +0 -0
  151. {sciris-3.5.2 → sciris-3.6.0}/tests/run_tests +0 -0
  152. {sciris-3.5.2 → sciris-3.6.0}/tests/sc_test_utils.py +0 -0
  153. {sciris-3.5.2 → sciris-3.6.0}/tests/showcase_sciris.py +0 -0
  154. {sciris-3.5.2 → sciris-3.6.0}/tests/showcase_vanilla.py +0 -0
  155. {sciris-3.5.2 → sciris-3.6.0}/tests/test_api.py +0 -0
  156. {sciris-3.5.2 → sciris-3.6.0}/tests/test_asd.py +0 -0
  157. {sciris-3.5.2 → sciris-3.6.0}/tests/test_colors.py +0 -0
  158. {sciris-3.5.2 → sciris-3.6.0}/tests/test_dataframe.py +0 -0
  159. {sciris-3.5.2 → sciris-3.6.0}/tests/test_datetime.py +0 -0
  160. {sciris-3.5.2 → sciris-3.6.0}/tests/test_fileio.py +0 -0
  161. {sciris-3.5.2 → sciris-3.6.0}/tests/test_math.py +0 -0
  162. {sciris-3.5.2 → sciris-3.6.0}/tests/test_nested.py +0 -0
  163. {sciris-3.5.2 → sciris-3.6.0}/tests/test_parallel.py +0 -0
  164. {sciris-3.5.2 → sciris-3.6.0}/tests/test_plotting.py +0 -0
  165. {sciris-3.5.2 → sciris-3.6.0}/tests/test_printing.py +0 -0
  166. {sciris-3.5.2 → sciris-3.6.0}/tests/test_profiling.py +0 -0
  167. {sciris-3.5.2 → sciris-3.6.0}/tests/test_settings.py +0 -0
  168. {sciris-3.5.2 → sciris-3.6.0}/tests/test_utils.py +0 -0
  169. {sciris-3.5.2 → sciris-3.6.0}/tests/test_versioning.py +0 -0
@@ -4,6 +4,15 @@ 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.6.0 (2026-10-19)
8
+
9
+ 1. `sc.obj` is now the class name, and `sc.objdict` an alias for it (previously the reverse). Files saved with earlier versions of Sciris still load, but files saved with this version need Sciris 3.5.0 or later to load. New code should use `sc.obj()` rather than `sc.objdict()`.
10
+ 1. `sc.obj()` has a new repr: short ones are shown on one line, e.g. `sc.obj(a=3, b=5)`, and longer ones with one key per line, in the same keyword style. `sc.obj.disp()` still shows the previous numbered format. Subclasses (e.g. `Pars` in Starsim) show their own class name.
11
+ 1. `sc.obj()` is faster: creating one is about twice as fast, setting a key about 40% faster, indexing by position (`obj[0]`) about twice as fast, and failed attribute lookups (e.g. `hasattr(obj, 'x')`) about 6 times faster. Pickles and copies no longer include the cached list of keys.
12
+ 1. Keys of `sc.obj()` now tab-complete in IPython and Jupyter.
13
+ 1. `sc.obj(index=3)` and other keyword arguments with the same name as a method now raise a warning, since e.g. `obj.index` returns the method rather than the key.
14
+ 1. Merging an `sc.odict()` or `sc.obj()` with a dict using `|` now returns the same type, rather than a plain `dict`.
15
+
7
16
  ## Version 3.5.2 (2026-10-16)
8
17
 
9
18
  1. `sc.urlopen()` and `sc.download()` have a new `timeout` argument (default 30 seconds; `None` waits forever, as before), and a new `progress` argument to show a progress bar.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sciris
3
- Version: 3.5.2
3
+ Version: 3.6.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
@@ -3,12 +3,13 @@ The "odict" class, combining features from an OrderedDict and a list/array.
3
3
 
4
4
  Highlights:
5
5
  - `sc.odict()`: flexible container representing the best-of-all-worlds across dicts, lists, and arrays
6
- - `objdict`: like an odict, but allows get/set via e.g. `foo.bar` instead of `foo['bar']`
6
+ - `sc.obj()`: like an odict, but allows get/set via e.g. `foo.bar` instead of `foo['bar']`
7
7
  """
8
8
  import re
9
9
  import sys
10
10
  import json
11
11
  import copyreg
12
+ import warnings
12
13
  import numpy as np
13
14
  import collections as co
14
15
  import sciris as sc
@@ -22,6 +23,9 @@ ddict = co.defaultdict # Define alias
22
23
  # Critical attributes that should be resolved immediately, not via any special handling
23
24
  _fundamental_attrs = frozenset(['__class__', '__reduce__', '__reduce_ex__', '__getstate__', '__setstate__', '__copy__','__deepcopy__'])
24
25
 
26
+ # Attribute names of each sc.obj class, cached so keys can be checked against them quickly
27
+ _class_attrs = {}
28
+
25
29
 
26
30
  class counter(co.Counter):
27
31
  """
@@ -147,15 +151,14 @@ class odict(dict):
147
151
  """ Combine the init properties of both OrderedDict and defaultdict """
148
152
 
149
153
  # Standard init
150
- mapping = dict(*args, **kwargs)
151
- dict.update(self, mapping)
154
+ dict.__init__(self, *args, **kwargs)
152
155
 
153
156
  # Handle defaultdict
154
157
  if defaultdict is not None:
155
158
  if defaultdict != 'nested' and not callable(defaultdict): # pragma: no cover
156
159
  errormsg = f'The defaultdict argument must be either "nested" or callable, not {type(defaultdict)}'
157
160
  raise TypeError(errormsg)
158
- self._setattr('_defaultdict', defaultdict) # Use object.__setattr__() since setattr() is overridden by sc.objdict()
161
+ self._setattr('_defaultdict', defaultdict) # Use object.__setattr__() since setattr() is overridden by sc.obj()
159
162
 
160
163
  return
161
164
 
@@ -166,6 +169,14 @@ class odict(dict):
166
169
  return cls(*args, **kwargs)
167
170
 
168
171
 
172
+ def __getstate__(self):
173
+ """ Don't pickle or copy the cached keys """
174
+ state = self.__dict__.copy()
175
+ state.pop('_cached_keys', None)
176
+ state.pop('_stale', None)
177
+ return state
178
+
179
+
169
180
  def _cache_keys(self):
170
181
  """ Store a copy of the keys as a list so integer lookup doesn't have to regenerate it each time """
171
182
  self._setattr('_cached_keys', self.keys())
@@ -439,10 +450,28 @@ class odict(dict):
439
450
 
440
451
  def _repr_pretty_(self, p, cycle, *args, **kwargs): # pragma: no cover
441
452
  """ Function to fix __repr__ in IPython"""
442
- print(self.__repr__(*args, **kwargs))
453
+ p.text(self.__repr__(*args, **kwargs))
443
454
  return
444
455
 
445
456
 
457
+ def __or__(self, other):
458
+ """ Merge with `|`, returning the same type as this dict """
459
+ if not isinstance(other, dict):
460
+ return NotImplemented
461
+ new = self.copy()
462
+ new.update(other)
463
+ return new
464
+
465
+
466
+ def __ror__(self, other):
467
+ """ Merge with `|` when this dict is on the right, e.g. `{'a':1} | sc.odict(b=2)` """
468
+ if not isinstance(other, dict):
469
+ return NotImplemented
470
+ new = self._new(other)
471
+ new.update(self)
472
+ return new
473
+
474
+
446
475
  def __add__(self, dict2):
447
476
  """
448
477
  Allow two dictionaries to be added (merged).
@@ -1253,7 +1282,7 @@ class odict(dict):
1253
1282
  #%% Other classes
1254
1283
  ##############################################################################
1255
1284
 
1256
- class objdict(odict):
1285
+ class obj(odict):
1257
1286
  """
1258
1287
  An `odict` that acts like an object -- allow keys to be set/retrieved by object
1259
1288
  notation.
@@ -1262,7 +1291,7 @@ class objdict(odict):
1262
1291
  instead act on dict keys (e.g. `obj['x'] = 3`). If you want to actually get/set
1263
1292
  an attribute, use `obj.getattribute()`/`obj.setattribute()`.
1264
1293
 
1265
- `sc.obj()` is an alias for `sc.objdict()`.
1294
+ `sc.objdict()` is an alias for `sc.obj()`.
1266
1295
 
1267
1296
  For a lighter-weight example (an object that acts like a dict), see `sc.dictobj()`.
1268
1297
 
@@ -1271,12 +1300,12 @@ class objdict(odict):
1271
1300
  ```python
1272
1301
  import sciris as sc
1273
1302
 
1274
- obj = sc.objdict(foo=3, bar=2)
1303
+ obj = sc.obj(foo=3, bar=2)
1275
1304
  obj.foo + obj.bar # Gives 5
1276
1305
  for key in obj.keys(): # It's still a dict
1277
1306
  obj[key] = 10
1278
1307
 
1279
- od = sc.objdict({'height':1.65, 'mass':59})
1308
+ od = sc.obj({'height':1.65, 'mass':59})
1280
1309
  od.bmi = od.mass/od.height**2
1281
1310
  od['bmi'] = od['mass']/od['height']**2 # Vanilla syntax still works
1282
1311
  od.keys = 3 # This raises an exception (you can't overwrite the keys() method)
@@ -1287,21 +1316,93 @@ class objdict(odict):
1287
1316
  `sc.dictobj()`.
1288
1317
 
1289
1318
  - *New in version 3.5.0:* `sc.obj()` alias; much faster attribute access
1319
+ - *New in version 3.6.0:* `sc.obj` is the class and `sc.objdict` the alias; faster creation, integer indexing, and failed attribute lookups
1290
1320
  """
1291
1321
 
1292
1322
  def __init__(self, *args, **kwargs):
1293
- nested_parent = kwargs.pop('_nested_parent', None)
1294
- nested_attr = kwargs.pop('_nested_attr', None)
1295
- if nested_parent is not None:
1296
- object.__setattr__(self, '_nested_parent', nested_parent)
1297
- object.__setattr__(self, '_nested_attr', nested_attr)
1298
- odict.__init__(self, *args, **kwargs) # Standard init
1323
+ odict.__init__(self, *args, **kwargs)
1324
+ if kwargs: # Warn if a keyword argument, e.g. sc.obj(index=3), can't be accessed as an attribute
1325
+ attrs = _class_attrs.get(type(self))
1326
+ if attrs is None:
1327
+ attrs = _class_attrs[type(self)] = frozenset(dir(type(self)))
1328
+ for key in attrs.intersection(kwargs):
1329
+ warnmsg = f'Key "{key}" has the same name as an attribute of {type(self).__name__}, so obj.{key} will not return it; use obj["{key}"] instead'
1330
+ warnings.warn(warnmsg, category=UserWarning, stacklevel=2)
1299
1331
  return
1300
1332
 
1301
1333
 
1302
1334
  def __repr__(self, *args, **kwargs):
1303
- """ Use odict repr, but with a custom class name and no quotes """
1304
- return odict.__repr__(self, quote='', numsep='.', classname=self.__class__.__name__, *args, **kwargs)
1335
+ """
1336
+ Show e.g. `sc.obj(a=1, b=2)`, or one key per line if that's too long
1337
+
1338
+ If arguments are supplied (e.g. `sigfigs`), use the `sc.odict()` format instead.
1339
+ """
1340
+ if args or kwargs:
1341
+ return odict.__repr__(self, *args, quote='', numsep='.', classname=type(self).__name__, **kwargs)
1342
+ return self._repr()
1343
+
1344
+
1345
+ def _repr(self, width=80, maxitems=200, _seen=None):
1346
+ """ Create the repr, recursing into child objs """
1347
+ name = 'sc.obj' if type(self) is obj else type(self).__name__
1348
+ if not len(self):
1349
+ return f'{name}()'
1350
+ if _seen is None:
1351
+ _seen = set()
1352
+ if id(self) in _seen: # Handle an obj that contains itself
1353
+ return f'{name}(...)'
1354
+ _seen = _seen | {id(self)}
1355
+
1356
+ # Get the keys and values to show
1357
+ keys = self.keys()
1358
+ nkeys = len(keys)
1359
+ if nkeys > maxitems:
1360
+ half = maxitems//2
1361
+ keys = keys[:half] + keys[-half:]
1362
+ kwstyle = all(isinstance(k, str) and k.isidentifier() for k in keys) # Use keyword style, e.g. a=1, if possible
1363
+ keystrs = keys if kwstyle else [repr(k) for k in keys]
1364
+ keylen = max(len(k) for k in keystrs)
1365
+ valwidth = width - keylen - 7 # Room left on the line after the indent and key
1366
+ valstrs = []
1367
+ for key in keys:
1368
+ val = dict.__getitem__(self, key)
1369
+ try:
1370
+ if isinstance(val, obj):
1371
+ valstr = val._repr(width=valwidth, maxitems=maxitems, _seen=_seen)
1372
+ elif sc.isnumber(val):
1373
+ valstr = str(val) # Avoid e.g. "np.float64(0.5)"
1374
+ else:
1375
+ valstr = repr(val)
1376
+ except Exception as E: # pragma: no cover
1377
+ valstr = f'<{type(val).__name__} repr failed: {E}>'
1378
+ valstrs.append(valstr)
1379
+
1380
+ # Try the short form, e.g. sc.obj(a=1, b=2)
1381
+ if kwstyle:
1382
+ short = f'{name}(' + ', '.join(f'{k}={v}' for k,v in zip(keystrs, valstrs)) + ')'
1383
+ else:
1384
+ short = f'{name}({{' + ', '.join(f'{k}: {v}' for k,v in zip(keystrs, valstrs)) + '})'
1385
+ if nkeys <= maxitems and len(short) <= width and '\n' not in short:
1386
+ return short
1387
+
1388
+ # Otherwise, show one key per line
1389
+ lines = [f'{name}(' if kwstyle else f'{name}({{']
1390
+ for i,(keystr,valstr) in enumerate(zip(keystrs, valstrs)):
1391
+ if nkeys > maxitems and i == half:
1392
+ lines.append(f' # [{nkeys - maxitems} additional items not shown]')
1393
+ prefix = f' {keystr:<{keylen}} = ' if kwstyle else f' {keystr+":":<{keylen+1}} '
1394
+ if isinstance(dict.__getitem__(self, keys[i]), obj):
1395
+ valstr = valstr.replace('\n', '\n ') # Indent child objs by one level
1396
+ else:
1397
+ valstr = valstr.replace('\n', '\n' + ' '*len(prefix)) # Align e.g. arrays with the start of the value
1398
+ lines.append(f'{prefix}{valstr},')
1399
+ lines.append(')' if kwstyle else '})')
1400
+ return '\n'.join(lines)
1401
+
1402
+
1403
+ def __dir__(self):
1404
+ """ Include keys, for tab completion """
1405
+ return object.__dir__(self) + [k for k in dict.keys(self) if isinstance(k, str) and k.isidentifier()]
1305
1406
 
1306
1407
 
1307
1408
  def __getattr__(self, attr):
@@ -1311,11 +1412,10 @@ class objdict(odict):
1311
1412
  return dict.__getitem__(self, attr)
1312
1413
  except KeyError:
1313
1414
  pass
1415
+ if self.__dict__.get('_defaultdict') is not None:
1416
+ return self.__getitem__(attr)
1314
1417
  errormsg = f"'{type(self).__name__}' object has no attribute '{attr}'"
1315
- exception = AttributeError(errormsg, name=attr, obj=self)
1316
- if attr in _fundamental_attrs:
1317
- raise exception
1318
- return self.__getitem__(attr, exception=exception) # Handle defaults, or else raise the exception
1418
+ raise AttributeError(errormsg, name=attr, obj=self)
1319
1419
 
1320
1420
 
1321
1421
  def __setattr__(self, name, value):
@@ -1329,33 +1429,36 @@ class objdict(odict):
1329
1429
  return self.__setitem__(name, value)
1330
1430
 
1331
1431
 
1332
- def __getitem__(self, attr, exception=None):
1432
+ def __getitem__(self, attr):
1333
1433
  """ Handle dict keys, including nested defaultdict logic """
1334
1434
  try: # Try retrieving normally
1335
1435
  return dict.__getitem__(self, attr)
1336
1436
  except Exception:
1337
1437
  pass
1338
- try: # Then try integers, slices, etc.
1339
- return odict.__getitem__(self, attr, allow_default=False) # Do not allow odict to handle default
1340
- except Exception as E2: # If that fails, raise the original exception
1341
- _defaultdict = self.__dict__.get('_defaultdict') # Handle defaultdict behavior by first checking if it exists
1342
- if _defaultdict is not None: # If it does, use it, then get the key again
1343
- if _defaultdict == 'nested':
1344
- return self._new(defaultdict='nested', _nested_parent=self, _nested_attr=attr) # Create a recursive object with links back to the ultimate parent
1345
- else:
1346
- dd = _defaultdict() # Create the new object
1347
- self[attr] = dd # Since we don't know what this object is, we can't be clever about recursion
1348
- return dd
1349
- else:
1350
- if exception:
1351
- raise exception
1352
- else:
1353
- raise E2
1438
+ if isinstance(attr, sc._numtype): # Integer index
1439
+ return dict.__getitem__(self, self._ikey(attr))
1440
+ _defaultdict = self.__dict__.get('_defaultdict') # Handle defaultdict behavior by first checking if it exists
1441
+ if _defaultdict is None or not isinstance(attr, (str, tuple)):
1442
+ return odict.__getitem__(self, attr, allow_default=False) # Slices, lists, etc., or raise the KeyError
1443
+ elif _defaultdict == 'nested': # Create a recursive object with links back to the ultimate parent
1444
+ dd = self._new(defaultdict='nested')
1445
+ object.__setattr__(dd, '_nested_parent', self)
1446
+ object.__setattr__(dd, '_nested_attr', attr)
1447
+ return dd
1448
+ else:
1449
+ dd = _defaultdict() # Create the new object
1450
+ self[attr] = dd # Since we don't know what this object is, we can't be clever about recursion
1451
+ return dd
1354
1452
 
1355
1453
 
1356
1454
  def __setitem__(self, name, value):
1357
1455
  """ Set as a dict item """
1358
- odict.__setitem__(self, name, value)
1456
+ if isinstance(name, str): # Fast path for the usual case
1457
+ if not dict.__contains__(self, name):
1458
+ object.__setattr__(self, '_stale', True) # Flag to refresh the cached keys
1459
+ dict.__setitem__(self, name, value)
1460
+ else:
1461
+ odict.__setitem__(self, name, value)
1359
1462
  attrs = self.__dict__
1360
1463
  if '_nested_parent' in attrs:
1361
1464
  attrs['_nested_parent'][attrs['_nested_attr']] = self
@@ -1381,7 +1484,7 @@ class objdict(odict):
1381
1484
  def setattribute(self, name, value, force=False):
1382
1485
  """ Set attribute if truly desired """
1383
1486
  if hasattr(self.__class__, name) and not force:
1384
- errormsg = f'objdict attribute "{name}" is read-only'
1487
+ errormsg = f'{type(self).__name__} attribute "{name}" is read-only'
1385
1488
  raise AttributeError(errormsg)
1386
1489
  return object.__setattr__(self, name, value)
1387
1490
 
@@ -1391,7 +1494,7 @@ class objdict(odict):
1391
1494
  return object.__delattr__(self, name)
1392
1495
 
1393
1496
 
1394
- obj = objdict # Define alias
1497
+ objdict = obj # Define alias
1395
1498
 
1396
1499
 
1397
1500
  class dictobj(dict):
@@ -1406,7 +1509,7 @@ class dictobj(dict):
1406
1509
  obj['b'] = 10
1407
1510
  print(obj.items())
1408
1511
  ```
1409
- For a more powerful alternative, see `sc.objdict()`.
1512
+ For a more powerful alternative, see `sc.obj()`.
1410
1513
 
1411
1514
  The keys and the attributes are the same thing (the dict is its own `__dict__`),
1412
1515
  so it is as fast as a plain object for attributes and nearly as fast as a plain
@@ -1530,7 +1633,7 @@ def asobj(obj, strict=True):
1530
1633
  return objobj(obj)
1531
1634
 
1532
1635
 
1533
- class argparse(objdict):
1636
+ class argparse(obj):
1534
1637
  """ Ultra-simple argument parser
1535
1638
 
1536
1639
  Accepts positional or keyword arguments, and converts them to the correct type.
@@ -1542,7 +1645,7 @@ class argparse(objdict):
1542
1645
  **kwargs (dict): keyword arguments to add to the parser
1543
1646
 
1544
1647
  Returns:
1545
- args (objdict): a dictionary-like object with the arguments
1648
+ args (sc.obj): a dictionary-like object with the arguments
1546
1649
 
1547
1650
  **Examples**:
1548
1651
 
@@ -4,6 +4,6 @@ Version and license information.
4
4
 
5
5
  __all__ = ['__version__', '__versiondate__', '__license__']
6
6
 
7
- __version__ = '3.5.2'
8
- __versiondate__ = '2026-10-16'
7
+ __version__ = '3.6.0'
8
+ __versiondate__ = '2026-10-19'
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.5.2
3
+ Version: 3.6.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
@@ -147,6 +147,16 @@ def test_repr():
147
147
  qq.disp(maxitems=15)
148
148
  qq[:] = 4
149
149
  assert all([v==4 for v in qq.values()])
150
+
151
+ # Test obj repr: short on one line, long with one key per line
152
+ assert repr(sc.obj(a=3, b=sc.obj(c=5))) == 'sc.obj(a=3, b=sc.obj(c=5))'
153
+ assert repr(sc.obj({'my key':1})) == "sc.obj({'my key': 1})"
154
+ long = sc.obj(x=np.arange(20).reshape(2,10), y=sc.obj(z='a string long enough to go over the line width'))
155
+ print(long)
156
+ assert repr(long).splitlines()[0] == 'sc.obj('
157
+ selfref = sc.obj(a=1)
158
+ selfref.b = selfref
159
+ assert repr(selfref) == 'sc.obj(a=1, b=sc.obj(...))'
150
160
  return
151
161
 
152
162
 
@@ -296,6 +306,10 @@ def test_asobj():
296
306
  myobjdict.setattribute('keys', 4)
297
307
 
298
308
  assert sc.obj is sc.objdict # Alias
309
+ assert 'x' in dir(sc.obj(x=1)) # For tab completion
310
+ assert isinstance(sc.obj(a=1) | {'b':2}, sc.obj) and isinstance({'b':2} | sc.obj(a=1), sc.obj)
311
+ with pytest.warns(UserWarning):
312
+ sc.obj(index=3) # Key can't be accessed as an attribute
299
313
 
300
314
  print('Testing dictobj')
301
315
  obj = sc.dictobj(x=4, y=6)
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes