sciris 3.4.0__tar.gz → 3.5.1__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.1}/CHANGELOG.md +14 -0
- {sciris-3.4.0 → sciris-3.5.1}/CLAUDE.md +1 -1
- {sciris-3.4.0/sciris.egg-info → sciris-3.5.1}/PKG-INFO +1 -1
- {sciris-3.4.0 → sciris-3.5.1}/docs/.gitignore +5 -0
- {sciris-3.4.0 → sciris-3.5.1}/docs/index.qmd +1 -1
- {sciris-3.4.0 → sciris-3.5.1}/docs/make_api.py +4 -47
- {sciris-3.4.0 → sciris-3.5.1}/docs/tutorials/tut_dicts.qmd +13 -1
- {sciris-3.4.0 → sciris-3.5.1}/sciris/sc_datetime.py +24 -12
- {sciris-3.4.0 → sciris-3.5.1}/sciris/sc_nested.py +38 -37
- {sciris-3.4.0 → sciris-3.5.1}/sciris/sc_odict.py +63 -69
- {sciris-3.4.0 → sciris-3.5.1}/sciris/sc_version.py +2 -2
- {sciris-3.4.0 → sciris-3.5.1/sciris.egg-info}/PKG-INFO +1 -1
- {sciris-3.4.0 → sciris-3.5.1}/sciris.egg-info/SOURCES.txt +1 -3
- sciris-3.5.1/sciris_issues.md +21 -0
- {sciris-3.4.0 → sciris-3.5.1}/tests/test_api.py +1 -16
- {sciris-3.4.0 → sciris-3.5.1}/tests/test_datetime.py +7 -0
- {sciris-3.4.0 → sciris-3.5.1}/tests/test_nested.py +2 -0
- {sciris-3.4.0 → sciris-3.5.1}/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.1}/.claude-plugin/marketplace.json +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/.github/workflows/publish_docs.yaml +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/.github/workflows/pypi_release.yaml +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/.github/workflows/schedule_tests.yaml +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/.github/workflows/test_docs.yaml +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/.github/workflows/test_downstream.yaml +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/.github/workflows/test_sciris.yaml +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/.gitignore +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/.pylintrc +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/.pytest_cache/README.md +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/CODE_OF_CONDUCT.md +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/CONTRIBUTING.md +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/LICENSE +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/MANIFEST.in +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/README.md +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/STYLE_GUIDE.md +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/claude_plugin/.claude-plugin/plugin.json +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/claude_plugin/README.md +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/claude_plugin/skills/sciris-advanced/SKILL.md +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/claude_plugin/skills/sciris-arrays/SKILL.md +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/claude_plugin/skills/sciris-dates/SKILL.md +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/claude_plugin/skills/sciris-dicts/SKILL.md +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/claude_plugin/skills/sciris-files/SKILL.md +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/claude_plugin/skills/sciris-intro/SKILL.md +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/claude_plugin/skills/sciris-parallel/SKILL.md +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/claude_plugin/skills/sciris-plotting/SKILL.md +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/claude_plugin/skills/sciris-printing/SKILL.md +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/claude_plugin/skills/sciris-utils/SKILL.md +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/context7.json +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/docs/404.qmd +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/docs/CNAME +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/docs/README.md +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/docs/_extensions/machow/interlinks/objects.txt +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/docs/_quarto.yml +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/docs/_site/llms-full.txt +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/docs/_site/llms.txt +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/docs/_site/robots.txt +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/docs/_variables.yml +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/docs/api/index.qmd +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/docs/assets/favicon.ico +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/docs/assets/sciris-logo-dark.png +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/docs/assets/sciris-logo-grey.png +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/docs/assets/sciris-logo-large.png +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/docs/assets/sciris-logo.png +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/docs/assets/sciris-showcase-code.png +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/docs/assets/sciris-showcase-output.png +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/docs/assets/styles-dark.scss +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/docs/assets/styles-light.scss +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/docs/assets/styles.css +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/docs/check_notebooks.py +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/docs/clean_all +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/docs/clean_outputs.py +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/docs/conduct.qmd +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/docs/contributing.qmd +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/docs/overview.qmd +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/docs/preview +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/docs/publish +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/docs/quarto_utils.py +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/docs/render +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/docs/requirements.txt +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/docs/sciris-citation.bib +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/docs/style_guide.qmd +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/docs/tutorials/.jupyter_cache/__version__.txt +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/docs/tutorials/README.md +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/docs/tutorials/convert_notebooks +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/docs/tutorials/dancing_lines.gif +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/docs/tutorials/img-advanced.gif +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/docs/tutorials/img-arrays.png +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/docs/tutorials/img-dates.png +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/docs/tutorials/img-dicts.png +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/docs/tutorials/img-files.png +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/docs/tutorials/img-intro.png +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/docs/tutorials/img-parallel.png +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/docs/tutorials/img-plotting.png +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/docs/tutorials/img-printing.png +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/docs/tutorials/img-utils.png +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/docs/tutorials/sim1/sim.py +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/docs/tutorials/sim2/sim.py +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/docs/tutorials/tut_advanced.qmd +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/docs/tutorials/tut_arrays.qmd +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/docs/tutorials/tut_dates.qmd +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/docs/tutorials/tut_files.qmd +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/docs/tutorials/tut_intro.qmd +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/docs/tutorials/tut_parallel.qmd +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/docs/tutorials/tut_plotting.qmd +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/docs/tutorials/tut_printing.qmd +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/docs/tutorials/tut_utils.qmd +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/docs/tutorials.qmd +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/docs/whatsnew.qmd +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/pyproject.toml +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/sciris/README.md +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/sciris/__init__.py +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/sciris/_extras/README.md +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/sciris/_extras/__init__.py +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/sciris/_extras/ansicolors.py +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/sciris/fancy.mplstyle +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/sciris/sc_asd.py +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/sciris/sc_colors.py +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/sciris/sc_dataframe.py +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/sciris/sc_fileio.py +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/sciris/sc_math.py +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/sciris/sc_parallel.py +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/sciris/sc_plotting.py +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/sciris/sc_printing.py +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/sciris/sc_profiling.py +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/sciris/sc_settings.py +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/sciris/sc_utils.py +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/sciris/sc_versioning.py +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/sciris/simple.mplstyle +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/sciris.egg-info/dependency_links.txt +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/sciris.egg-info/requires.txt +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/sciris.egg-info/top_level.txt +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/setup.cfg +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/tests/.coveragerc +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/tests/.pytest_cache/README.md +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/tests/README.rst +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/tests/check_coverage +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/tests/check_style +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/tests/files/archive_2023-04-19.zip +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/tests/files/deadclass.obj +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/tests/files/deadclass.py +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/tests/files/exampledata.xlsx +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/tests/files/examplefont.ttf +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/tests/files/make_archive.py +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/tests/files/make_deadclass.py +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/tests/files/pickle_2021-01-01.obj +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/tests/files/pickle_2022-01-01.obj +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/tests/pytest.ini +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/tests/regression/README.rst +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/tests/regression/run_downstream +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/tests/regression/run_regression +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/tests/regression/sciris2021-01-01.yml +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/tests/regression/sciris2022-01-01.yml +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/tests/regression/sciris2023-01-01.yml +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/tests/regression/setup_envs +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/tests/requirements.txt +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/tests/run_tests +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/tests/sc_test_utils.py +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/tests/showcase_sciris.py +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/tests/showcase_vanilla.py +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/tests/test_asd.py +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/tests/test_colors.py +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/tests/test_dataframe.py +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/tests/test_fileio.py +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/tests/test_math.py +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/tests/test_parallel.py +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/tests/test_plotting.py +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/tests/test_printing.py +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/tests/test_profiling.py +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/tests/test_settings.py +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/tests/test_utils.py +0 -0
- {sciris-3.4.0 → sciris-3.5.1}/tests/test_versioning.py +0 -0
|
@@ -4,6 +4,20 @@ 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.1 (2026-10-15)
|
|
8
|
+
|
|
9
|
+
1. `sc.iterobj()` and `sc.search()` are about twice as fast; `sc.iterobj()` no longer descends into classes or NumPy dtypes stored as attributes (e.g. `self.dtype = np.int64`), since their attributes belong to the class rather than the object. To restore the previous behavior, pass `atomic=sc.sc_nested.atomic_with_tuple`.
|
|
10
|
+
1. `sc.search()` no longer matches the root object when searching by key.
|
|
11
|
+
1. `sc.yeartodate()` is about 6 times faster, and now accepts a list or array of years, returning a list of dates (about 40 ns per date).
|
|
12
|
+
|
|
13
|
+
## Version 3.5.0 (2026-10-13)
|
|
14
|
+
|
|
15
|
+
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.
|
|
16
|
+
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.
|
|
17
|
+
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.
|
|
18
|
+
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.
|
|
19
|
+
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.
|
|
20
|
+
|
|
7
21
|
## Version 3.4.0 (2026-09-28)
|
|
8
22
|
|
|
9
23
|
### 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
|
|
|
@@ -662,29 +662,41 @@ def yeartodate(year, as_date=True, **kwargs):
|
|
|
662
662
|
Convert a decimal year to a date
|
|
663
663
|
|
|
664
664
|
Args:
|
|
665
|
-
year (int, float): The numerical year to convert to a
|
|
666
|
-
as_date (bool): If True (default), return
|
|
665
|
+
year (int, float, list, array): The numerical year (or years) to convert to a date
|
|
666
|
+
as_date (bool): If True (default), return a `datetime.date` object, otherwise return a string
|
|
667
667
|
|
|
668
668
|
Returns:
|
|
669
|
-
|
|
669
|
+
A `datetime.date` object (default) or string, depending on the `as_date` argument; a list of these if a list or array of years is supplied
|
|
670
670
|
|
|
671
|
-
**
|
|
671
|
+
**Examples**:
|
|
672
672
|
|
|
673
673
|
```python
|
|
674
674
|
sc.yeartodate(2010.5) # Returns datetime.date(2010, 7, 2)
|
|
675
|
+
sc.yeartodate(np.arange(2020, 2021, 1/52)) # Returns a list of weekly dates
|
|
675
676
|
```
|
|
676
677
|
|
|
677
678
|
- *New in version 3.2.1.*
|
|
679
|
+
- *New in version 3.5.1:* faster; list and array inputs
|
|
678
680
|
"""
|
|
679
681
|
as_date = kwargs.pop('asdate', as_date) # Handle with or without underscore
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
out =
|
|
682
|
+
|
|
683
|
+
if isinstance(year, (list, tuple, np.ndarray)): # Handle multiple years
|
|
684
|
+
years = np.asarray(year, dtype=float)
|
|
685
|
+
full_years = years.astype(int) # Truncate, like int()
|
|
686
|
+
starts = (full_years - 1970).astype('datetime64[Y]') # Start of each year
|
|
687
|
+
year_days = ((starts + 1).astype('datetime64[D]') - starts.astype('datetime64[D]')).astype(int)
|
|
688
|
+
days = np.round((years - full_years)*year_days).astype(int)
|
|
689
|
+
out = (starts.astype('datetime64[D]') + days).tolist() # Convert to a list of datetime.date objects
|
|
690
|
+
if not as_date:
|
|
691
|
+
out = [str(d) for d in out]
|
|
692
|
+
else:
|
|
693
|
+
full_years = int(year)
|
|
694
|
+
remainder = year - full_years
|
|
695
|
+
year_days = _get_year_length(full_years).days
|
|
696
|
+
days = int(np.round(remainder*year_days))
|
|
697
|
+
out = dt.date(year=full_years, month=1, day=1) + dt.timedelta(days=days)
|
|
698
|
+
if not as_date:
|
|
699
|
+
out = str(out)
|
|
688
700
|
return out
|
|
689
701
|
|
|
690
702
|
|
|
@@ -20,6 +20,7 @@ from .sc_utils import _not_given, _is_given # Allows searching for actual None v
|
|
|
20
20
|
# Define objects for which it doesn't make sense to descend further -- used here and sc.equal()
|
|
21
21
|
atomic_classes = [np.ndarray, pd.Series, pd.DataFrame, pd.core.indexes.base.Index]
|
|
22
22
|
atomic_with_tuple = atomic_classes + [tuple]
|
|
23
|
+
atomic_types = [type, np.dtype] # Classes and dtypes are also not descended into by sc.iterobj(), but are compared normally by sc.equal()
|
|
23
24
|
|
|
24
25
|
|
|
25
26
|
##############################################################################
|
|
@@ -402,7 +403,7 @@ class IterObj:
|
|
|
402
403
|
# Attributes with initialization required
|
|
403
404
|
self._trace = []
|
|
404
405
|
self._memo = co.defaultdict(int)
|
|
405
|
-
self.output =
|
|
406
|
+
self.output = {} # Converted to an objdict once the iteration is complete, since it's much faster to populate a dict
|
|
406
407
|
if self.func is None: # If no function provided, define a function that just returns the contents of the current node
|
|
407
408
|
self.func = lambda obj: obj
|
|
408
409
|
|
|
@@ -411,15 +412,15 @@ class IterObj:
|
|
|
411
412
|
atomic_list = sc.tolist(self.atomic)
|
|
412
413
|
if 'default' in atomic_list: # Handle objects to not descend into
|
|
413
414
|
atomic_list.remove('default')
|
|
414
|
-
base_atomic = atomic_with_tuple
|
|
415
|
+
base_atomic = atomic_with_tuple + atomic_types
|
|
415
416
|
if 'default-tuple' in atomic_list:
|
|
416
417
|
atomic_list.remove('default-tuple')
|
|
417
|
-
base_atomic = atomic_classes
|
|
418
|
+
base_atomic = atomic_classes + atomic_types
|
|
418
419
|
self.atomic = tuple(base_atomic + atomic_list)
|
|
419
420
|
|
|
420
421
|
# Handle objects to skip
|
|
421
422
|
if isinstance(skip, dict):
|
|
422
|
-
skip =
|
|
423
|
+
skip = dict(skip) # Shallow copy since the values are only read
|
|
423
424
|
skip_keys = sc.tolist(skip.pop('keys', None))
|
|
424
425
|
skip_ids = sc.tolist(skip.pop('ids', None))
|
|
425
426
|
skip_subclasses = sc.tolist(skip.pop('subclasses', None))
|
|
@@ -481,9 +482,10 @@ class IterObj:
|
|
|
481
482
|
print(space*len(self._trace) + string)
|
|
482
483
|
return
|
|
483
484
|
|
|
484
|
-
def iteritems(self, parent, trace):
|
|
485
|
+
def iteritems(self, parent, trace, itertype=None):
|
|
485
486
|
""" Return an iterator over items in this object """
|
|
486
|
-
itertype
|
|
487
|
+
if itertype is None:
|
|
488
|
+
itertype = self.check_iter_type(parent)
|
|
487
489
|
out = None
|
|
488
490
|
if self.custom_iter:
|
|
489
491
|
out = self.custom_iter(parent)
|
|
@@ -505,9 +507,7 @@ class IterObj:
|
|
|
505
507
|
out = [] # Return nothing if doesn't have __dict__.items() or __slots__, e.g. a weird wrapped function
|
|
506
508
|
else:
|
|
507
509
|
out = [] # Return nothing if not recognized
|
|
508
|
-
out =
|
|
509
|
-
for i in range(len(out)):
|
|
510
|
-
out[i] = [parent, trace, *list(out[i])] # Prepend parent and trace to the arguments
|
|
510
|
+
out = [(parent, trace, k, v) for k,v in out] # Prepend parent and trace to the arguments
|
|
511
511
|
return out
|
|
512
512
|
|
|
513
513
|
def getitem(self, key, parent):
|
|
@@ -542,12 +542,14 @@ class IterObj:
|
|
|
542
542
|
""" Shortcut to check_iter_type() """
|
|
543
543
|
return check_iter_type(obj, known=self.atomic, custom=self.custom_type)
|
|
544
544
|
|
|
545
|
-
def check_proceed(self, key, subobj, newid):
|
|
545
|
+
def check_proceed(self, key, subobj, newid, itertype=None):
|
|
546
546
|
""" Check if we should process the object, and if so, whether to descend into it """
|
|
547
547
|
|
|
548
548
|
# If we've already parsed this (iterable) object, skip it, or if aliases=True, process it but don't descend into it again
|
|
549
549
|
in_memo = (newid in self._memo) and (self._memo[newid] > self.recursion)
|
|
550
|
-
|
|
550
|
+
if in_memo and itertype is None:
|
|
551
|
+
itertype = self.check_iter_type(subobj)
|
|
552
|
+
repeat = in_memo and bool(itertype)
|
|
551
553
|
memo_skip = repeat and not self.aliases
|
|
552
554
|
descend = not repeat
|
|
553
555
|
if repeat and self.aliases:
|
|
@@ -560,22 +562,23 @@ class IterObj:
|
|
|
560
562
|
instance_skip = isinstance(subobj, self._skip_instances)
|
|
561
563
|
|
|
562
564
|
# Finalize
|
|
563
|
-
|
|
564
|
-
proceed = not any(skips)
|
|
565
|
+
proceed = not (memo_skip or key_skip or id_skip or subclass_skip or instance_skip)
|
|
565
566
|
|
|
566
567
|
if not proceed and self.verbose: # Just for debugging
|
|
568
|
+
skips = [memo_skip, key_skip, id_skip, subclass_skip, instance_skip]
|
|
567
569
|
labels = ['memo', 'key', 'id', 'subclass', 'instance']
|
|
568
570
|
pairs = [f'{label}_skip=True' for label,skip in zip(labels, skips) if skip]
|
|
569
571
|
self.indent(f'Skipping "{key}" because {sc.strjoin(pairs)}')
|
|
570
572
|
|
|
571
573
|
return proceed, descend
|
|
572
574
|
|
|
573
|
-
def process_obj(self, parent, trace, key, subobj, newid):
|
|
575
|
+
def process_obj(self, parent, trace, key, subobj, newid, itertype=None):
|
|
574
576
|
""" Process a single object, returning its trace and its new value """
|
|
575
577
|
self._memo[newid] += 1
|
|
576
578
|
trace = trace + [key]
|
|
577
|
-
subitertype = self.check_iter_type(subobj)
|
|
578
|
-
self.
|
|
579
|
+
subitertype = self.check_iter_type(subobj) if itertype is None else itertype
|
|
580
|
+
if self.verbose: # Check here to avoid formatting the string
|
|
581
|
+
self.indent(f'{len(self)} Trace {trace} | Type "{str(subitertype)}" | {type(subobj)}')
|
|
579
582
|
newobj = subobj
|
|
580
583
|
if not (self.leaf and subitertype):
|
|
581
584
|
newobj = self.func(subobj, **self.func_kw)
|
|
@@ -604,11 +607,15 @@ class IterObj:
|
|
|
604
607
|
while queue:
|
|
605
608
|
parent,trace,key,subobj = queue.popleft()
|
|
606
609
|
newid = id(subobj)
|
|
607
|
-
|
|
610
|
+
itertype = self.check_iter_type(subobj) # Calculate once, since it's needed several times
|
|
611
|
+
proceed, descend = self.check_proceed(key, subobj, newid, itertype=itertype)
|
|
608
612
|
if proceed:
|
|
609
|
-
newtrace, newobj = self.process_obj(parent, trace, key, subobj, newid) # Process the object
|
|
613
|
+
newtrace, newobj = self.process_obj(parent, trace, key, subobj, newid, itertype=itertype) # Process the object
|
|
610
614
|
if descend: # If modifying in place, descend into the new object, since that's what's in the parent now
|
|
611
|
-
|
|
615
|
+
if newobj is subobj or not self.inplace:
|
|
616
|
+
newitems = self.iteritems(subobj, newtrace, itertype=itertype)
|
|
617
|
+
else:
|
|
618
|
+
newitems = self.iteritems(newobj, newtrace)
|
|
612
619
|
if self.depthfirst:
|
|
613
620
|
queue.extendleft(reversed(newitems)) # extendleft() swaps order, so swap back
|
|
614
621
|
else:
|
|
@@ -620,6 +627,7 @@ class IterObj:
|
|
|
620
627
|
return self.func(self.obj, **self.func_kw) # Set at the root level
|
|
621
628
|
return self.obj
|
|
622
629
|
else:
|
|
630
|
+
self.output = sc.objdict(self.output)
|
|
623
631
|
return self.output
|
|
624
632
|
|
|
625
633
|
def flatten_traces(self, sep='_', inplace=True):
|
|
@@ -675,7 +683,7 @@ def iterobj(obj, func=None, inplace=False, copy=False, leaf=False, recursion=0,
|
|
|
675
683
|
leaf (bool): whether to apply the function only to leaf nodes of the object
|
|
676
684
|
recursion (int): number of recursive steps to allow, i.e. parsing the same objects multiple times (default 0)
|
|
677
685
|
depthfirst (bool): whether to parse the object depth-first (default) or breadth-first
|
|
678
|
-
atomic (list): a list of known classes to treat as atomic (do not descend into); if 'default', use defaults (e.g. tuple, `np.array`, `pd.DataFrame
|
|
686
|
+
atomic (list): a list of known classes to treat as atomic (do not descend into); if 'default', use defaults (e.g. tuple, `np.array`, `pd.DataFrame`, classes, and NumPy dtypes); if 'default-tuple', use defaults except for tuples
|
|
679
687
|
skip (list/dict): a list of objects to skip over entirely; can also be a dict with "keys", "ids", "subclasses", and/or "instances", which skip each of those
|
|
680
688
|
rootkey (str): the key to list as the root of the object (default `'root'`)
|
|
681
689
|
verbose (bool): whether to print progress
|
|
@@ -716,6 +724,7 @@ def iterobj(obj, func=None, inplace=False, copy=False, leaf=False, recursion=0,
|
|
|
716
724
|
- *New in version 3.1.5:* "recursion" argument; better handling of atomic classes
|
|
717
725
|
- *New in version 3.1.6:* "skip", "depthfirst", "to_df", and "flatten" arguments
|
|
718
726
|
- *New in version 3.4.0:* "aliases" argument
|
|
727
|
+
- *New in version 3.5.1:* classes and NumPy dtypes are atomic by default
|
|
719
728
|
"""
|
|
720
729
|
# Create the object
|
|
721
730
|
io = IterObj(obj=obj, func=func, inplace=inplace, copy=copy, leaf=leaf, recursion=recursion, aliases=aliases, depthfirst=depthfirst,
|
|
@@ -962,23 +971,15 @@ def search(obj, query=_not_given, key=_not_given, value=_not_given, type=_not_gi
|
|
|
962
971
|
flatten = kwargs.pop('flatten', False) # Don't flatten because that will disrupt the matching
|
|
963
972
|
tree = iterobj(obj, **kwargs) # For key matching
|
|
964
973
|
|
|
965
|
-
# Do the matching
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
# Match values (including types)
|
|
975
|
-
if _is_given(value):
|
|
976
|
-
for k,v in tree.items():
|
|
977
|
-
if check_match(v, value):
|
|
978
|
-
matches.append(k)
|
|
979
|
-
|
|
980
|
-
# Reassemble dict to maintain order
|
|
981
|
-
out = sc.objdict({k:v for k,v in tree.items() if k in matches})
|
|
974
|
+
# Do the matching, keeping the order of the tree
|
|
975
|
+
match_key = _is_given(key)
|
|
976
|
+
match_value = _is_given(value)
|
|
977
|
+
out = {}
|
|
978
|
+
for k,v in tree.items():
|
|
979
|
+
is_root = not isinstance(k, tuple) # The root has no key to match
|
|
980
|
+
if (match_key and not is_root and check_match(k[-1], key)) or (match_value and check_match(v, value)): # Only want the last key of the trace
|
|
981
|
+
out[k] = v
|
|
982
|
+
out = sc.objdict(out)
|
|
982
983
|
|
|
983
984
|
if flatten:
|
|
984
985
|
out = flatten_traces(out)
|
|
@@ -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):
|