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.
- {sciris-3.5.2 → sciris-3.6.0}/CHANGELOG.md +9 -0
- {sciris-3.5.2/sciris.egg-info → sciris-3.6.0}/PKG-INFO +1 -1
- {sciris-3.5.2 → sciris-3.6.0}/sciris/sc_odict.py +147 -44
- {sciris-3.5.2 → sciris-3.6.0}/sciris/sc_version.py +2 -2
- {sciris-3.5.2 → sciris-3.6.0/sciris.egg-info}/PKG-INFO +1 -1
- {sciris-3.5.2 → sciris-3.6.0}/tests/test_odict.py +14 -0
- {sciris-3.5.2 → sciris-3.6.0}/.claude-plugin/marketplace.json +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/.github/workflows/publish_docs.yaml +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/.github/workflows/pypi_release.yaml +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/.github/workflows/schedule_tests.yaml +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/.github/workflows/test_docs.yaml +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/.github/workflows/test_downstream.yaml +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/.github/workflows/test_sciris.yaml +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/.gitignore +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/.pylintrc +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/.pytest_cache/README.md +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/CLAUDE.md +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/CODE_OF_CONDUCT.md +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/CONTRIBUTING.md +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/LICENSE +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/MANIFEST.in +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/README.md +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/STYLE_GUIDE.md +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/claude_plugin/.claude-plugin/plugin.json +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/claude_plugin/README.md +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/claude_plugin/skills/sciris-advanced/SKILL.md +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/claude_plugin/skills/sciris-arrays/SKILL.md +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/claude_plugin/skills/sciris-dates/SKILL.md +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/claude_plugin/skills/sciris-dicts/SKILL.md +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/claude_plugin/skills/sciris-files/SKILL.md +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/claude_plugin/skills/sciris-intro/SKILL.md +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/claude_plugin/skills/sciris-parallel/SKILL.md +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/claude_plugin/skills/sciris-plotting/SKILL.md +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/claude_plugin/skills/sciris-printing/SKILL.md +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/claude_plugin/skills/sciris-utils/SKILL.md +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/context7.json +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/docs/.gitignore +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/docs/404.qmd +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/docs/CNAME +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/docs/README.md +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/docs/_extensions/machow/interlinks/objects.txt +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/docs/_quarto.yml +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/docs/_site/llms-full.txt +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/docs/_site/llms.txt +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/docs/_site/robots.txt +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/docs/_variables.yml +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/docs/api/index.qmd +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/docs/assets/favicon.ico +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/docs/assets/sciris-logo-dark.png +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/docs/assets/sciris-logo-grey.png +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/docs/assets/sciris-logo-large.png +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/docs/assets/sciris-logo.png +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/docs/assets/sciris-showcase-code.png +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/docs/assets/sciris-showcase-output.png +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/docs/assets/styles-dark.scss +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/docs/assets/styles-light.scss +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/docs/assets/styles.css +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/docs/check_notebooks.py +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/docs/clean_all +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/docs/clean_outputs.py +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/docs/conduct.qmd +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/docs/contributing.qmd +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/docs/index.qmd +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/docs/make_api.py +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/docs/overview.qmd +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/docs/preview +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/docs/publish +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/docs/quarto_utils.py +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/docs/render +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/docs/requirements.txt +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/docs/sciris-citation.bib +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/docs/style_guide.qmd +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/docs/tutorials/.jupyter_cache/__version__.txt +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/docs/tutorials/README.md +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/docs/tutorials/convert_notebooks +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/docs/tutorials/dancing_lines.gif +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/docs/tutorials/img-advanced.gif +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/docs/tutorials/img-arrays.png +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/docs/tutorials/img-dates.png +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/docs/tutorials/img-dicts.png +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/docs/tutorials/img-files.png +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/docs/tutorials/img-intro.png +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/docs/tutorials/img-parallel.png +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/docs/tutorials/img-plotting.png +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/docs/tutorials/img-printing.png +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/docs/tutorials/img-utils.png +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/docs/tutorials/sim1/sim.py +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/docs/tutorials/sim2/sim.py +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/docs/tutorials/tut_advanced.qmd +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/docs/tutorials/tut_arrays.qmd +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/docs/tutorials/tut_dates.qmd +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/docs/tutorials/tut_dicts.qmd +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/docs/tutorials/tut_files.qmd +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/docs/tutorials/tut_intro.qmd +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/docs/tutorials/tut_parallel.qmd +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/docs/tutorials/tut_plotting.qmd +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/docs/tutorials/tut_printing.qmd +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/docs/tutorials/tut_utils.qmd +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/docs/tutorials.qmd +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/docs/whatsnew.qmd +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/pyproject.toml +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/sciris/README.md +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/sciris/__init__.py +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/sciris/_extras/README.md +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/sciris/_extras/__init__.py +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/sciris/_extras/ansicolors.py +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/sciris/fancy.mplstyle +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/sciris/sc_asd.py +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/sciris/sc_colors.py +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/sciris/sc_dataframe.py +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/sciris/sc_datetime.py +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/sciris/sc_fileio.py +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/sciris/sc_math.py +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/sciris/sc_nested.py +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/sciris/sc_parallel.py +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/sciris/sc_plotting.py +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/sciris/sc_printing.py +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/sciris/sc_profiling.py +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/sciris/sc_settings.py +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/sciris/sc_utils.py +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/sciris/sc_versioning.py +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/sciris/simple.mplstyle +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/sciris.egg-info/SOURCES.txt +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/sciris.egg-info/dependency_links.txt +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/sciris.egg-info/requires.txt +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/sciris.egg-info/top_level.txt +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/setup.cfg +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/tests/.coveragerc +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/tests/.pytest_cache/README.md +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/tests/README.rst +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/tests/check_coverage +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/tests/check_style +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/tests/files/archive_2023-04-19.zip +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/tests/files/deadclass.obj +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/tests/files/deadclass.py +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/tests/files/exampledata.xlsx +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/tests/files/examplefont.ttf +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/tests/files/make_archive.py +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/tests/files/make_deadclass.py +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/tests/files/pickle_2021-01-01.obj +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/tests/files/pickle_2022-01-01.obj +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/tests/pytest.ini +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/tests/regression/README.rst +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/tests/regression/run_downstream +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/tests/regression/run_regression +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/tests/regression/sciris2021-01-01.yml +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/tests/regression/sciris2022-01-01.yml +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/tests/regression/sciris2023-01-01.yml +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/tests/regression/setup_envs +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/tests/requirements.txt +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/tests/run_tests +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/tests/sc_test_utils.py +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/tests/showcase_sciris.py +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/tests/showcase_vanilla.py +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/tests/test_api.py +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/tests/test_asd.py +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/tests/test_colors.py +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/tests/test_dataframe.py +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/tests/test_datetime.py +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/tests/test_fileio.py +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/tests/test_math.py +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/tests/test_nested.py +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/tests/test_parallel.py +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/tests/test_plotting.py +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/tests/test_printing.py +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/tests/test_profiling.py +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/tests/test_settings.py +0 -0
- {sciris-3.5.2 → sciris-3.6.0}/tests/test_utils.py +0 -0
- {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.
|
|
@@ -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
|
-
- `
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
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
|
-
"""
|
|
1304
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
1339
|
-
return
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
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
|
-
|
|
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'
|
|
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
|
-
|
|
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.
|
|
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(
|
|
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 (
|
|
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.
|
|
8
|
-
__versiondate__ = '2026-10-
|
|
7
|
+
__version__ = '3.6.0'
|
|
8
|
+
__versiondate__ = '2026-10-19'
|
|
9
9
|
__license__ = f'Sciris {__version__} ({__versiondate__}) – © 2014-2026 by the Sciris Development Team'
|
|
@@ -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
|
|
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
|