Skip to content

Fix dotted dict processing dots in second+ level keys#2745

Merged
rchl merged 4 commits intomainfrom
fix/dotted-dict
Feb 4, 2026
Merged

Fix dotted dict processing dots in second+ level keys#2745
rchl merged 4 commits intomainfrom
fix/dotted-dict

Conversation

@rchl
Copy link
Member

@rchl rchl commented Feb 1, 2026

Make DottedDict only process top-level keys as "dotted".

Some examples:

  1. Same as before
DottedDict({
    "a.b": {
        "c": 1
    }
})

=>

{
    "a": {
        "b": {
            "c": 1
        }
    }
}
  1. Doesn't expand second level anymore
DottedDict({
    "a.b": {
        "c.d": 1
    }
})

=>

{
    "a": {
        "b": {
            "c.d": 1
        }
    }
}

  • DottedDict.update(other) also works like that - other only gets keys expanded at the first level.
  • DottedDict.set(path, other) works as before - the whole path is expanded

(there is a bit of repetition in the code (_merge() and set() are pretty similar) and not sure about naming of merge since we sometimes call it "update".

Fixes #2744

Copy link
Member

@jwortmann jwortmann left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new tests don't actually test what was fixed here.
I would expect something like

d = DottedDict({"editor.codeActionsOnSave": {"source.fixAll": "explicit"}})
self.assertIsNone(d.get("editor.codeActionsOnSave.source"))
self.assertIsNone(d.get("editor.codeActionsOnSave.source.fixAll"))

Note that the second test shows that we cannot access individual nested keys with a dot in it anymore, but I guess that is fine.

@jwortmann
Copy link
Member

Note that the second test shows that we cannot access individual nested keys with a dot in it anymore, but I guess that is fine.

Actually I think this could be solved as well by recursively testing the full key name first, before splitting parts from the right end (I haven't tried a concrete implementation).

current: Any = self._d
keys = path.split('.')
for key in keys:
if isinstance(current, dict):
current = current.get(key)
else:
return None
return current

Then if a language server asks for "section": "editor.codeActionsOnSave.source.fixAll" we could return the value "explicit".

@rchl
Copy link
Member Author

rchl commented Feb 2, 2026

Interesting point you are bringing up, I didn't think of "get" not working for those.

I'm curious how it works in VSCode when server asks. Only if there is an interoperability issues I would consider changing it.

@rchl
Copy link
Member Author

rchl commented Feb 2, 2026

I see the same behavior in VSCode:

For a setting like:

{
    "editor.codeActionsOnSave": {
        "source.fixAll.eslint": "always"
    },
}

here are relevant requests:


[Trace - 5:05:06 PM] Received request 'workspace/configuration - (2)'.
Params: {
    "items": [
        {
            "scopeUri": "file:///Users/rafal/Downloads/text.txt",
            "section": "editor.codeActionsOnSave"
        }
    ]
}


[Trace - 5:05:06 PM] Sending response 'workspace/configuration - (2)'. Processing request took 0ms
Result: [
    {
        "source.fixAll.eslint": "always"
    }
]


[Trace - 5:05:06 PM] Received request 'workspace/configuration - (3)'.
Params: {
    "items": [
        {
            "scopeUri": "file:///Users/rafal/Downloads/text.txt",
            "section": "editor.codeActionsOnSave.source"
        }
    ]
}


[Trace - 5:05:06 PM] Sending response 'workspace/configuration - (3)'. Processing request took 0ms
Result: [
    null
]


[Trace - 5:05:06 PM] Received request 'workspace/configuration - (4)'.
Params: {
    "items": [
        {
            "scopeUri": "file:///Users/rafal/Downloads/text.txt",
            "section": "editor.codeActionsOnSave.source.fixAll.eslint"
        }
    ]
}


[Trace - 5:05:06 PM] Sending response 'workspace/configuration - (4)'. Processing request took 0ms
Result: [
    null
]

@jwortmann
Copy link
Member

[Trace - 5:05:06 PM] Received request 'workspace/configuration - (4)'.
Params: {
    "items": [
        {
            "scopeUri": "file:///Users/rafal/Downloads/text.txt",
            "section": "editor.codeActionsOnSave.source.fixAll.eslint"
        }
    ]
}

I assume you created this request just for testing, or is there a real server that uses a section like this?

Here is what I was thinking about in my comment above, how it might work:
(I have not tested if it really works, and probably it doesn't, but perhaps you get the idea what I had in my mind)

    def get(self, path: str | None = None) -> Any:
        """
        Get a value from the dictionary.

        :param      path:  The path, e.g. foo.bar.baz, or None.

        :returns:   The value stored at the path, or None if it doesn't exist.
                    Note that this cannot distinguish between None values and
                    paths that don't exist. If the path is None, returns the
                    entire dictionary.
        """
        return self._d if path is None else self._get(self._d, path)

    @staticmethod
    def _get(d: dict[str, Any], path: str) -> Any:
        segments = path.split('.')
        for idx in range(len(segments), 0, -1):
            key = '.'.join(segments[:idx])
            if key in d:
                if rest := '.'.join(segments[idx:]):
                    val = d[key]
                    if isinstance(val, dict):
                        return DottedDict._get(val, rest)
                    return None
                return d[key]
        return None

@rchl
Copy link
Member Author

rchl commented Feb 2, 2026

I've tested with a real server based on https://github.com/microsoft/vscode-extension-samples/tree/main/lsp-sample that I've just extended to ask for different sections.

I don't see any point in trying to make it work differently than how it works in VSCode.
Or, if having a more flexible DottedDict is useful than we can make it configurable how it works and have both behaviors.

That reminds me that I should verify that any server settings or custom plugin code currently doesn't rely on old behavior...

@rchl
Copy link
Member Author

rchl commented Feb 3, 2026

Found two packages that would be affected. Those will be fixed.

@rchl rchl merged commit 1b0545f into main Feb 4, 2026
8 checks passed
@rchl rchl deleted the fix/dotted-dict branch February 4, 2026 20:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

DottedDict processing dot when it shouldn't

2 participants