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.
- {sciris-3.4.0 → sciris-3.5.0}/CHANGELOG.md +8 -0
- {sciris-3.4.0 → sciris-3.5.0}/CLAUDE.md +1 -1
- {sciris-3.4.0/sciris.egg-info → sciris-3.5.0}/PKG-INFO +1 -1
- {sciris-3.4.0 → sciris-3.5.0}/docs/.gitignore +5 -0
- {sciris-3.4.0 → sciris-3.5.0}/docs/index.qmd +1 -1
- {sciris-3.4.0 → sciris-3.5.0}/docs/make_api.py +4 -47
- {sciris-3.4.0 → sciris-3.5.0}/docs/tutorials/tut_dicts.qmd +13 -1
- sciris-3.5.0/objdict_performance.md +89 -0
- {sciris-3.4.0 → sciris-3.5.0}/sciris/sc_nested.py +2 -1
- {sciris-3.4.0 → sciris-3.5.0}/sciris/sc_odict.py +63 -69
- {sciris-3.4.0 → sciris-3.5.0}/sciris/sc_version.py +2 -2
- {sciris-3.4.0 → sciris-3.5.0/sciris.egg-info}/PKG-INFO +1 -1
- {sciris-3.4.0 → sciris-3.5.0}/sciris.egg-info/SOURCES.txt +1 -3
- {sciris-3.4.0 → sciris-3.5.0}/tests/test_api.py +1 -16
- {sciris-3.4.0 → sciris-3.5.0}/tests/test_odict.py +7 -0
- sciris-3.4.0/docs/api.json +0 -2824
- sciris-3.4.0/docs/llms-full.txt +0 -2120
- sciris-3.4.0/docs/llms.txt +0 -342
- {sciris-3.4.0 → sciris-3.5.0}/.claude-plugin/marketplace.json +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/.github/workflows/publish_docs.yaml +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/.github/workflows/pypi_release.yaml +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/.github/workflows/schedule_tests.yaml +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/.github/workflows/test_docs.yaml +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/.github/workflows/test_downstream.yaml +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/.github/workflows/test_sciris.yaml +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/.gitignore +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/.pylintrc +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/.pytest_cache/README.md +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/CODE_OF_CONDUCT.md +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/CONTRIBUTING.md +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/LICENSE +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/MANIFEST.in +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/README.md +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/STYLE_GUIDE.md +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/claude_plugin/.claude-plugin/plugin.json +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/claude_plugin/README.md +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/claude_plugin/skills/sciris-advanced/SKILL.md +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/claude_plugin/skills/sciris-arrays/SKILL.md +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/claude_plugin/skills/sciris-dates/SKILL.md +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/claude_plugin/skills/sciris-dicts/SKILL.md +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/claude_plugin/skills/sciris-files/SKILL.md +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/claude_plugin/skills/sciris-intro/SKILL.md +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/claude_plugin/skills/sciris-parallel/SKILL.md +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/claude_plugin/skills/sciris-plotting/SKILL.md +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/claude_plugin/skills/sciris-printing/SKILL.md +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/claude_plugin/skills/sciris-utils/SKILL.md +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/context7.json +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/docs/404.qmd +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/docs/CNAME +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/docs/README.md +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/docs/_extensions/machow/interlinks/objects.txt +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/docs/_quarto.yml +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/docs/_site/llms-full.txt +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/docs/_site/llms.txt +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/docs/_site/robots.txt +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/docs/_variables.yml +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/docs/api/index.qmd +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/docs/assets/favicon.ico +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/docs/assets/sciris-logo-dark.png +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/docs/assets/sciris-logo-grey.png +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/docs/assets/sciris-logo-large.png +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/docs/assets/sciris-logo.png +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/docs/assets/sciris-showcase-code.png +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/docs/assets/sciris-showcase-output.png +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/docs/assets/styles-dark.scss +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/docs/assets/styles-light.scss +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/docs/assets/styles.css +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/docs/check_notebooks.py +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/docs/clean_all +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/docs/clean_outputs.py +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/docs/conduct.qmd +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/docs/contributing.qmd +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/docs/overview.qmd +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/docs/preview +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/docs/publish +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/docs/quarto_utils.py +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/docs/render +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/docs/requirements.txt +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/docs/sciris-citation.bib +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/docs/style_guide.qmd +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/docs/tutorials/.jupyter_cache/__version__.txt +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/docs/tutorials/README.md +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/docs/tutorials/convert_notebooks +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/docs/tutorials/dancing_lines.gif +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/docs/tutorials/img-advanced.gif +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/docs/tutorials/img-arrays.png +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/docs/tutorials/img-dates.png +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/docs/tutorials/img-dicts.png +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/docs/tutorials/img-files.png +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/docs/tutorials/img-intro.png +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/docs/tutorials/img-parallel.png +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/docs/tutorials/img-plotting.png +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/docs/tutorials/img-printing.png +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/docs/tutorials/img-utils.png +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/docs/tutorials/sim1/sim.py +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/docs/tutorials/sim2/sim.py +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/docs/tutorials/tut_advanced.qmd +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/docs/tutorials/tut_arrays.qmd +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/docs/tutorials/tut_dates.qmd +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/docs/tutorials/tut_files.qmd +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/docs/tutorials/tut_intro.qmd +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/docs/tutorials/tut_parallel.qmd +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/docs/tutorials/tut_plotting.qmd +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/docs/tutorials/tut_printing.qmd +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/docs/tutorials/tut_utils.qmd +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/docs/tutorials.qmd +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/docs/whatsnew.qmd +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/pyproject.toml +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/sciris/README.md +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/sciris/__init__.py +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/sciris/_extras/README.md +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/sciris/_extras/__init__.py +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/sciris/_extras/ansicolors.py +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/sciris/fancy.mplstyle +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/sciris/sc_asd.py +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/sciris/sc_colors.py +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/sciris/sc_dataframe.py +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/sciris/sc_datetime.py +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/sciris/sc_fileio.py +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/sciris/sc_math.py +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/sciris/sc_parallel.py +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/sciris/sc_plotting.py +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/sciris/sc_printing.py +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/sciris/sc_profiling.py +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/sciris/sc_settings.py +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/sciris/sc_utils.py +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/sciris/sc_versioning.py +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/sciris/simple.mplstyle +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/sciris.egg-info/dependency_links.txt +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/sciris.egg-info/requires.txt +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/sciris.egg-info/top_level.txt +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/setup.cfg +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/tests/.coveragerc +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/tests/.pytest_cache/README.md +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/tests/README.rst +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/tests/check_coverage +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/tests/check_style +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/tests/files/archive_2023-04-19.zip +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/tests/files/deadclass.obj +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/tests/files/deadclass.py +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/tests/files/exampledata.xlsx +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/tests/files/examplefont.ttf +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/tests/files/make_archive.py +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/tests/files/make_deadclass.py +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/tests/files/pickle_2021-01-01.obj +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/tests/files/pickle_2022-01-01.obj +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/tests/pytest.ini +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/tests/regression/README.rst +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/tests/regression/run_downstream +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/tests/regression/run_regression +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/tests/regression/sciris2021-01-01.yml +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/tests/regression/sciris2022-01-01.yml +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/tests/regression/sciris2023-01-01.yml +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/tests/regression/setup_envs +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/tests/requirements.txt +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/tests/run_tests +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/tests/sc_test_utils.py +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/tests/showcase_sciris.py +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/tests/showcase_vanilla.py +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/tests/test_asd.py +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/tests/test_colors.py +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/tests/test_dataframe.py +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/tests/test_datetime.py +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/tests/test_fileio.py +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/tests/test_math.py +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/tests/test_nested.py +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/tests/test_parallel.py +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/tests/test_plotting.py +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/tests/test_printing.py +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/tests/test_profiling.py +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/tests/test_settings.py +0 -0
- {sciris-3.4.0 → sciris-3.5.0}/tests/test_utils.py +0 -0
- {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
|
-
- **
|
|
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
|
|
@@ -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
|
|
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
|
-
|
|
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'
|
|
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
|
-
|
|
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.
|
|
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 =
|
|
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
|
-
|
|
245
|
+
object.__setattr__(self, '_stale', True) # Flag to refresh the cached keys
|
|
244
246
|
|
|
245
247
|
if isinstance(key, (str,tuple)):
|
|
246
|
-
|
|
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
|
|
1302
|
-
"""
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
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
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
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
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
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(' +
|
|
1436
|
+
output = 'dictobj(' + dict.__repr__(self) + ')'
|
|
1425
1437
|
return output
|
|
1426
1438
|
|
|
1427
|
-
def
|
|
1428
|
-
"""
|
|
1429
|
-
return
|
|
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
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
return
|
|
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.
|
|
8
|
-
__versiondate__ = '2026-
|
|
7
|
+
__version__ = '3.5.0'
|
|
8
|
+
__versiondate__ = '2026-10-13'
|
|
9
9
|
__license__ = f'Sciris {__version__} ({__versiondate__}) – © 2014-2026 by the Sciris Development Team'
|
|
@@ -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 (
|
|
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
|
|