bdice: @vyasr Since the names of our CI checks changed with #266, you may need to merge `master` into this branch to make the required checks pass. I'm fine with you overriding the required checks if you want, since this PR is fairly small and doesn't touch anything altered in #266.
codecov[bot]: # [Codecov](https://codecov.io/gh/glotzerlab/signac/pull/269?src=pr&el=h1) Report
> Merging [#269](https://codecov.io/gh/glotzerlab/signac/pull/269?src=pr&el=desc) into [master](https://codecov.io/gh/glotzerlab/signac/commit/e5cd6726d1d6a1f0ea0f61d21b03dbaa744ab66a?src=pr&el=desc) will **not change** coverage.
> The diff coverage is `n/a`.

[![Impacted file tree graph](https://codecov.io/gh/glotzerlab/signac/pull/269/graphs/tree.svg?width=650&token=FghbjTyzgM&height=150&src=pr)](https://codecov.io/gh/glotzerlab/signac/pull/269?src=pr&el=tree)

```diff
@@           Coverage Diff           @@
##           master     #269   +/-   ##
=======================================
  Coverage   65.42%   65.42%           
=======================================
  Files          42       42           
  Lines        5697     5697           
=======================================
  Hits         3727     3727           
  Misses       1970     1970
```



------

[Continue to review full report at Codecov](https://codecov.io/gh/glotzerlab/signac/pull/269?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/glotzerlab/signac/pull/269?src=pr&el=footer). Last update [e5cd672...a3879da](https://codecov.io/gh/glotzerlab/signac/pull/269?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

csadorf: @vyasr Can you update the name of this PR to be a bit more descriptive?
vyasr: Yes, sorry it was a single commit so it just autopopulated the title and I didn't change it.
diff --git a/changelog.txt b/changelog.txt
index d099ad1a..bbd04bb9 100644
--- a/changelog.txt
+++ b/changelog.txt
@@ -19,6 +19,7 @@ Fixed
 +++++
 
  - Fixed issues on Windows with ``H5Store``, project import/export, and operations that move files (#264, #266).
+ - Calling ``items`` or ``values`` on _SyncedDict objects does not mutate nested dictionaries (#234, #269).
 
 
 [1.3.0] -- 2019-12-20
diff --git a/signac/core/synceddict.py b/signac/core/synceddict.py
index e3eb1393..48c74555 100644
--- a/signac/core/synceddict.py
+++ b/signac/core/synceddict.py
@@ -265,11 +265,11 @@ class _SyncedDict(MutableMapping):
 
     def values(self):
         self._synced_load()
-        return self._convert_to_dict(self._data).values()
+        return self._convert_to_dict(self).values()
 
     def items(self):
         self._synced_load()
-        return self._convert_to_dict(self._data).items()
+        return self._convert_to_dict(self).items()
 
     def __repr__(self):
         return repr(self())
@@ -279,7 +279,7 @@ class _SyncedDict(MutableMapping):
 
     def _as_dict(self):
         with self._suspend_sync():
-            return self._convert_to_dict(self._data.copy())
+            return self._convert_to_dict(self)
 
     def __call__(self):
         self._synced_load()

Calling `_SyncedDict.values()` gives `dict` objects
<!-- Please replace the text in the individual sections below. -->

### Description

Calling standard `MutableMapping` methods (`keys`, `values`, etc) on `_SyncedAttrDict` objects modifies the underlying dictionary in place. This behavior is unexpected, such an operation should only affect the returned value.

### To reproduce

```
>> import signac
>> project = signac.init_project('test')
>> job = project.open_job({'a': {'b': 'c'}}).init()
>> print(type(job.sp.a))
<class 'signac.core.attrdict.SyncedAttrDict'>
>> job.sp.values()
>> print(type(job.sp.a))
<class 'dict'>
```

There is an open question of whether `job.sp.values()` should return nested dictionaries as `SyncedDict` objects or normal `dict` objects. I prefer that `SyncedDict` objects be what it returns so that it's still going to sync, as does @bdice, but that is a separate question. Either way, calling values in the manner in this PR should not modify the object in place.

### System configuration

Linux-4.19.61-1-lts-x86_64-with-arch
3.7.4 (default, Oct  4 2019, 06:57:26)
[GCC 9.2.0]
1.2.0


diff --git a/tests/test_synced_attrdict.py b/tests/test_synced_attrdict.py
index 503c758d..6509e363 100644
--- a/tests/test_synced_attrdict.py
+++ b/tests/test_synced_attrdict.py
@@ -508,6 +508,18 @@ class SyncedAttrDictTest(unittest.TestCase):
         self.assertEqual(len(sad), 1)
         self.assert_only_read()
 
+    def test_nested_types_dict_conversion(self):
+        """Ensure that calling methods like items and values does not
+        change the type of nested dictionaries."""
+        sad = self.get_sad({'a': {'b': 1}})
+        assert type(sad['a']) is SAD
+        sad.items()
+        assert type(sad['a']) is SAD
+        sad.values()
+        assert type(sad['a']) is SAD
+        sad._as_dict()
+        assert type(sad['a']) is SAD
+
 
 if __name__ == '__main__':
     unittest.main()

pip install -e .[dev]
parse_log_pytest
requirements.txt
pytest
3.9
requirements.txt
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
deprecation==2.1.0
exceptiongroup==1.2.2
filelock==3.18.0
iniconfig==2.1.0
packaging==24.2
pluggy==1.5.0
pytest==8.3.5
-e git+https://github.com/glotzerlab/signac.git@dc870ac06159cae97e1475894913d1def567555e#egg=signac
tomli==2.2.1

name: signac
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=3.0.16=h5eee18b_0
  - pip=25.0=py39h06a4308_0
  - python=3.9.21=he870216_1
  - readline=8.2=h5eee18b_0
  - setuptools=75.8.0=py39h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - tzdata=2025a=h04d1e81_0
  - wheel=0.45.1=py39h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - deprecation==2.1.0
      - exceptiongroup==1.2.2
      - filelock==3.18.0
      - iniconfig==2.1.0
      - packaging==24.2
      - pluggy==1.5.0
      - pytest==8.3.5
      - tomli==2.2.1
prefix: /opt/conda/envs/signac

tests/test_synced_attrdict.py::SyncedAttrDictTest::test_nested_types_dict_conversion
tests/test_synced_attrdict.py::SyncedAttrDictTest::test_attr_reference_modification tests/test_synced_attrdict.py::SyncedAttrDictTest::test_call tests/test_synced_attrdict.py::SyncedAttrDictTest::test_clear tests/test_synced_attrdict.py::SyncedAttrDictTest::test_clear_sync tests/test_synced_attrdict.py::SyncedAttrDictTest::test_copy tests/test_synced_attrdict.py::SyncedAttrDictTest::test_copy_as_dict tests/test_synced_attrdict.py::SyncedAttrDictTest::test_copy_as_dict_sync tests/test_synced_attrdict.py::SyncedAttrDictTest::test_copy_value tests/test_synced_attrdict.py::SyncedAttrDictTest::test_delete tests/test_synced_attrdict.py::SyncedAttrDictTest::test_delete_sync tests/test_synced_attrdict.py::SyncedAttrDictTest::test_init tests/test_synced_attrdict.py::SyncedAttrDictTest::test_is_object_and_mapping tests/test_synced_attrdict.py::SyncedAttrDictTest::test_iter tests/test_synced_attrdict.py::SyncedAttrDictTest::test_iter_sync tests/test_synced_attrdict.py::SyncedAttrDictTest::test_list_modification tests/test_synced_attrdict.py::SyncedAttrDictTest::test_repr tests/test_synced_attrdict.py::SyncedAttrDictTest::test_set_get tests/test_synced_attrdict.py::SyncedAttrDictTest::test_set_get_attr_sync tests/test_synced_attrdict.py::SyncedAttrDictTest::test_set_get_explicit_nested tests/test_synced_attrdict.py::SyncedAttrDictTest::test_set_get_sync tests/test_synced_attrdict.py::SyncedAttrDictTest::test_str tests/test_synced_attrdict.py::SyncedAttrDictTest::test_suspend_sync tests/test_synced_attrdict.py::SyncedAttrDictTest::test_update tests/test_synced_attrdict.py::SyncedAttrDictTest::test_update_sync
signac/core/synceddict.py changelog.txt
signac/core/synceddict.py changelog.txt