Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 21 additions & 4 deletions src/core/IronPython/Runtime/Types/MappingProxy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,30 @@
using System.Collections.Generic;
using System.Diagnostics;

using Microsoft.Scripting.Runtime;

using IronPython.Runtime.Operations;

namespace IronPython.Runtime.Types {
[PythonType("mappingproxy")]
public sealed class MappingProxy : IDictionary<object?, object?>, IDictionary {
internal PythonDictionary GetDictionary(CodeContext context) => dictionary ?? type!.GetMemberDictionary(context, false);

private bool TryGetValue(CodeContext context, object? key, out object? value) {
if (dictionary is not null) {
return dictionary.TryGetValue(key, out value);
}

string? name = key as string ?? (key as Extensible<string>)?.Value;
if (name is not null && type!.TryLookupSlot(context, name, out PythonTypeSlot slot)) {
value = slot is PythonTypeUserDescriptorSlot dts ? dts.Value : slot;
return true;
}

value = null;
return false;
}

private readonly PythonDictionary? dictionary;
private readonly PythonType? type;

Expand All @@ -36,7 +53,7 @@ public MappingProxy([NotNone] PythonDictionary dict) {

public int __len__(CodeContext context) => GetDictionary(context).Count;

public bool __contains__(CodeContext/*!*/ context, object? value) => GetDictionary(context).TryGetValue(value, out _);
public bool __contains__(CodeContext/*!*/ context, object? value) => TryGetValue(context, value, out _);

public string/*!*/ __str__(CodeContext/*!*/ context) => DictionaryOps.__repr__(context, this);

Expand All @@ -58,7 +75,7 @@ public string __repr__(CodeContext/*!*/ context) {
}

public object? get(CodeContext/*!*/ context, object? k, object? d = null) {
if (GetDictionary(context).TryGetValue(k, out object? res)) {
if (TryGetValue(context, k, out object? res)) {
return res;
}

Expand Down Expand Up @@ -111,7 +128,7 @@ public object __eq__(CodeContext/*!*/ context, object? other) {
#region IDictionary Members

public object? this[object? key] {
get => GetDictionary(DefaultContext.Default)[key];
get => TryGetValue(DefaultContext.Default, key, out object? value) ? value : throw PythonOps.KeyError(key);
[PythonHidden]
set => throw PythonOps.TypeError("'mappingproxy' object does not support item assignment");
}
Expand Down Expand Up @@ -187,7 +204,7 @@ ICollection IDictionary.Values {

bool IDictionary<object?, object?>.Remove(object? key) => throw new InvalidOperationException("mappingproxy is read-only");

bool IDictionary<object?, object?>.TryGetValue(object? key, out object? value) => GetDictionary(DefaultContext.Default).TryGetValue(key, out value);
bool IDictionary<object?, object?>.TryGetValue(object? key, out object? value) => TryGetValue(DefaultContext.Default, key, out value);

ICollection<object?> IDictionary<object?, object?>.Values => GetDictionary(DefaultContext.Default).Values;

Expand Down
17 changes: 17 additions & 0 deletions tests/suite/test_dict.py
Original file line number Diff line number Diff line change
Expand Up @@ -1033,4 +1033,21 @@ def test_cp34770(self):
m[u64] = 'b'
self.assertEqual(m[9223372036854775808], 'b')

def test_mappingproxy_str_subclass_key(self):
class S(str): pass
class C:
x = 1

for d, key, value in ((str.__dict__, '__len__', str.__dict__['__len__']), (C.__dict__, 'x', 1)):
self.assertIn(S(key), d)
self.assertIs(d.get(S(key)), value)
self.assertIs(d[S(key)], value)
self.assertNotIn(S('nope'), d)
with self.assertRaises(KeyError):
d[S('nope')]

for key in (1, None, b'x'):
self.assertNotIn(key, C.__dict__)
self.assertIsNone(C.__dict__.get(key))

run_test(__name__)