// Copyright 2016 The Bazel Authors. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package com.google.devtools.build.lib.syntax; import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.Lists; import com.google.devtools.build.lib.events.Location; import com.google.devtools.build.lib.skylarkinterface.Param; import com.google.devtools.build.lib.skylarkinterface.SkylarkCallable; import com.google.devtools.build.lib.skylarkinterface.SkylarkModule; import com.google.devtools.build.lib.skylarkinterface.SkylarkModuleCategory; import com.google.devtools.build.lib.skylarkinterface.SkylarkPrinter; import com.google.devtools.build.lib.syntax.SkylarkList.MutableList; import com.google.devtools.build.lib.syntax.SkylarkList.Tuple; import com.google.devtools.build.lib.syntax.SkylarkMutable.MutableMap; import java.util.ArrayList; import java.util.Collections; import java.util.LinkedHashMap; import java.util.Map; import javax.annotation.Nullable; /** * A Skylark dictionary (dict). * *

Although this implements the {@link Map} interface, it is not mutable via that interface's * methods. Instead, use the mutators that take in a {@link Mutability} object. */ @SkylarkModule( name = "dict", category = SkylarkModuleCategory.BUILTIN, doc = "A language built-in type representating a dictionary (associative mapping). " + "Dictionaries may be constructed with a special literal syntax:
" + "

d = {\"a\": 2, \"b\": 5}
" + "See also the dict() constructor function. " + "When using the literal syntax, it is an error to have duplicated keys. " + "Use square brackets to access elements:
" + "
e = d[\"a\"]   # e == 2
" + "Like lists, they can also be constructed using a comprehension syntax:
" + "
d = {i: 2*i for i in range(20)}\n"
          + "e = d[8]       # e == 16
" + "Dictionaries are mutable. You can add new elements or mutate existing ones:" + "
d[\"key\"] = 5
" + "

Iterating over a dict is equivalent to iterating over its keys. The " + "in operator tests for membership in the keyset of the dict.
" + "

\"a\" in {\"a\" : 2, \"b\" : 5} "
          + "# evaluates as True
" + "The iteration order for a dict is deterministic and specified as the order in which " + "the keys have been added to the dict. The iteration order is not affected if a value " + "associated with an existing key is updated." ) public final class SkylarkDict extends MutableMap implements Map, SkylarkIndexable { private final LinkedHashMap contents = new LinkedHashMap<>(); /** Final except for {@link #unsafeShallowFreeze}; must not be modified any other way. */ private Mutability mutability; private SkylarkDict(@Nullable Mutability mutability) { this.mutability = mutability == null ? Mutability.IMMUTABLE : mutability; } private SkylarkDict(@Nullable Environment env) { this.mutability = env == null ? Mutability.IMMUTABLE : env.mutability(); } @SkylarkCallable(name = "get", doc = "Returns the value for key if key is in the dictionary, " + "else default. If default is not given, it defaults to " + "None, so that this method never throws an error.", parameters = { @Param(name = "key", noneable = true, doc = "The key to look for."), @Param(name = "default", defaultValue = "None", noneable = true, named = true, doc = "The default value to use (instead of None) if the key is not found.")}, allowReturnNones = true ) public Object get(Object key, Object defaultValue) { if (this.containsKey(key)) { return this.get(key); } return defaultValue; } @SkylarkCallable( name = "pop", doc = "Removes a key from the dict, and returns the associated value. " + "If entry with that key was found, return the specified default value;" + "if no default value was specified, fail instead.", parameters = { @Param(name = "key", type = Object.class, doc = "The key.", noneable = true), @Param(name = "default", type = Object.class, defaultValue = "unbound", named = true, noneable = true, doc = "a default value if the key is absent."), }, useLocation = true, useEnvironment = true ) public Object pop(Object key, Object defaultValue, Location loc, Environment env) throws EvalException { Object value = get(key); if (value != null) { remove(key, loc, env.mutability()); return value; } if (defaultValue != Runtime.UNBOUND) { return defaultValue; } throw new EvalException(loc, Printer.format("KeyError: %r", key)); } @SkylarkCallable( name = "popitem", doc = "Remove and return an arbitrary (key, value) pair from the dictionary. " + "popitem() is useful to destructively iterate over a dictionary, " + "as often used in set algorithms. " + "If the dictionary is empty, calling popitem() fails. " + "It is deterministic which pair is returned.", useLocation = true, useEnvironment = true ) public Tuple popitem(Location loc, Environment env) throws EvalException { if (isEmpty()) { throw new EvalException(loc, "popitem(): dictionary is empty"); } Object key = keySet().iterator().next(); Object value = get(key); remove(key, loc, env.mutability()); return Tuple.of(key, value); } @SkylarkCallable( name = "setdefault", doc = "If key is in the dictionary, return its value. " + "If not, insert key with a value of default " + "and return default. " + "default defaults to None.", parameters = { @Param(name = "key", type = Object.class, doc = "The key."), @Param( name = "default", type = Object.class, defaultValue = "None", named = true, noneable = true, doc = "a default value if the key is absent." ), }, useLocation = true, useEnvironment = true ) public Object setdefault( K key, V defaultValue, Location loc, Environment env) throws EvalException { Object value = get(key); if (value != null) { return value; } put(key, defaultValue, loc, env); return defaultValue; } @SkylarkCallable( name = "update", doc = "Update the dictionary with the key/value pairs from other, overwriting existing keys.", parameters = { @Param(name = "other", type = SkylarkDict.class, doc = "The values to add."), }, useLocation = true, useEnvironment = true ) public Runtime.NoneType update( SkylarkDict other, Location loc, Environment env) throws EvalException { putAll(other, loc, env.mutability()); return Runtime.NONE; } @SkylarkCallable( name = "values", doc = "Returns the list of values:" + "
"
            + "{2: \"a\", 4: \"b\", 1: \"c\"}.values() == [\"a\", \"b\", \"c\"]
\n", useEnvironment = true ) public MutableList invoke(Environment env) throws EvalException { return MutableList.copyOf(env, values()); } @SkylarkCallable( name = "items", doc = "Returns the list of key-value tuples:" + "
"
            + "{2: \"a\", 4: \"b\", 1: \"c\"}.items() == [(2, \"a\"), (4, \"b\"), (1, \"c\")]"
            + "
\n", useEnvironment = true ) public MutableList items(Environment env) throws EvalException { ArrayList list = Lists.newArrayListWithCapacity(size()); for (Map.Entry entries : entrySet()) { list.add(Tuple.of(entries.getKey(), entries.getValue())); } return MutableList.wrapUnsafe(env, list); } @SkylarkCallable(name = "keys", doc = "Returns the list of keys:" + "
{2: \"a\", 4: \"b\", 1: \"c\"}.keys() == [2, 4, 1]"
        + "
\n", useEnvironment = true ) public MutableList keys(Environment env) throws EvalException { ArrayList list = Lists.newArrayListWithCapacity(size()); for (Map.Entry entries : entrySet()) { list.add(entries.getKey()); } return MutableList.wrapUnsafe(env, list); } private static final SkylarkDict EMPTY = withMutability(Mutability.IMMUTABLE); /** Returns an immutable empty dict. */ // Safe because the empty singleton is immutable. @SuppressWarnings("unchecked") public static SkylarkDict empty() { return (SkylarkDict) EMPTY; } /** Returns an empty dict with the given {@link Mutability}. */ public static SkylarkDict withMutability(@Nullable Mutability mutability) { return new SkylarkDict<>(mutability); } /** @return a dict mutable in given environment only */ public static SkylarkDict of(@Nullable Environment env) { return new SkylarkDict<>(env); } /** @return a dict mutable in given environment only, with given initial key and value */ public static SkylarkDict of(@Nullable Environment env, K k, V v) { return SkylarkDict.of(env).putUnsafe(k, v); } /** @return a dict mutable in given environment only, with two given initial key value pairs */ public static SkylarkDict of( @Nullable Environment env, K k1, V v1, K k2, V v2) { return SkylarkDict.of(env).putUnsafe(k1, v1).putUnsafe(k2, v2); } // TODO(bazel-team): Make other methods that take in mutabilities instead of environments, make // this method public. @VisibleForTesting static SkylarkDict copyOf( @Nullable Mutability mutability, Map m) { return SkylarkDict.withMutability(mutability).putAllUnsafe(m); } /** @return a dict mutable in given environment only, with contents copied from given map */ public static SkylarkDict copyOf( @Nullable Environment env, Map m) { return SkylarkDict.of(env).putAllUnsafe(m); } /** Puts the given entry into the dict, without calling {@link #checkMutable}. */ private SkylarkDict putUnsafe(K k, V v) { contents.put(k, v); return this; } /** Puts all entries of the given map into the dict, without calling {@link #checkMutable}. */ @SuppressWarnings("unchecked") private SkylarkDict putAllUnsafe(Map m) { for (Map.Entry e : m.entrySet()) { contents.put(e.getKey(), (VV) SkylarkType.convertToSkylark(e.getValue(), mutability)); } return this; } @Override public Mutability mutability() { return mutability; } @Override public void unsafeShallowFreeze() { Mutability.Freezable.checkUnsafeShallowFreezePrecondition(this); this.mutability = Mutability.IMMUTABLE; } @Override protected Map getContentsUnsafe() { return contents; } /** * Puts an entry into a dict, after validating that mutation is allowed. * * @param key the key of the added entry * @param value the value of the added entry * @param loc the location to use for error reporting * @param mutability the {@link Mutability} associated with the opreation * @throws EvalException if the key is invalid or the dict is frozen */ public void put(K key, V value, Location loc, Mutability mutability) throws EvalException { checkMutable(loc, mutability); EvalUtils.checkValidDictKey(key); contents.put(key, value); } /** * Convenience version of {@link #put(K, V, Location, Mutability)} that uses the {@link * Mutability} of an {@link Environment}. */ // TODO(bazel-team): Decide whether to eliminate this overload. public void put(K key, V value, Location loc, Environment env) throws EvalException { put(key, value, loc, env.mutability()); } /** * Puts all the entries from a given map into the dict, after validating that mutation is allowed. * * @param map the map whose entries are added * @param loc the location to use for error reporting * @param mutability the {@link Mutability} associated with the operation * @throws EvalException if some key is invalid or the dict is frozen */ public void putAll( Map map, Location loc, Mutability mutability) throws EvalException { checkMutable(loc, mutability); for (Map.Entry e : map.entrySet()) { KK k = e.getKey(); EvalUtils.checkValidDictKey(k); contents.put(k, e.getValue()); } } /** * Deletes the entry associated with the given key. * * @param key the key to delete * @param loc the location to use for error reporting * @param mutability the {@link Mutability} associated with the operation * @return the value associated to the key, or {@code null} if not present * @throws EvalException if the dict is frozen */ V remove(Object key, Location loc, Mutability mutability) throws EvalException { checkMutable(loc, mutability); return contents.remove(key); } @SkylarkCallable( name = "clear", doc = "Remove all items from the dictionary.", useLocation = true, useEnvironment = true ) public Runtime.NoneType clearDict( Location loc, Environment env) throws EvalException { clear(loc, env.mutability()); return Runtime.NONE; } /** * Clears the dict. * * @param loc the location to use for error reporting * @param mutability the {@link Mutability} associated with the operation * @throws EvalException if the dict is frozen */ void clear(Location loc, Mutability mutability) throws EvalException { checkMutable(loc, mutability); contents.clear(); } @Override public void repr(SkylarkPrinter printer) { printer.printList(entrySet(), "{", ", ", "}", null); } @Override public String toString() { return Printer.repr(this); } /** * If {@code obj} is a {@code SkylarkDict}, casts it to an unmodifiable {@code Map} after * checking that each of its entries has key type {@code keyType} and value type {@code * valueType}. If {@code obj} is {@code None} or null, treats it as an empty dict. * *

The returned map may or may not be a view that is affected by updates to the original dict. * * @param obj the object to cast. null and None are treated as an empty dict. * @param keyType the expected type of all the dict's keys * @param valueType the expected type of all the dict's values * @param description a description of the argument being converted, or null, for debugging */ public static Map castSkylarkDictOrNoneToDict( Object obj, Class keyType, Class valueType, @Nullable String description) throws EvalException { if (EvalUtils.isNullOrNone(obj)) { return empty(); } if (obj instanceof SkylarkDict) { return ((SkylarkDict) obj).getContents(keyType, valueType, description); } throw new EvalException( null, String.format( "%s is not of expected type dict or NoneType", description == null ? Printer.repr(obj) : String.format("'%s'", description))); } /** * Casts this dict to an unmodifiable {@code SkylarkDict}, after checking that all keys and * values have types {@code keyType} and {@code valueType} respectively. * *

The returned map may or may not be a view that is affected by updates to the original dict. * * @param keyType the expected class of keys * @param valueType the expected class of values * @param description a description of the argument being converted, or null, for debugging */ @SuppressWarnings("unchecked") public Map getContents( Class keyType, Class valueType, @Nullable String description) throws EvalException { Object keyDescription = description == null ? null : Printer.formattable("'%s' key", description); Object valueDescription = description == null ? null : Printer.formattable("'%s' value", description); for (Map.Entry e : this.entrySet()) { SkylarkType.checkType(e.getKey(), keyType, keyDescription); SkylarkType.checkType(e.getValue(), valueType, valueDescription); } return Collections.unmodifiableMap((SkylarkDict) this); } @Override public final Object getIndex(Object key, Location loc) throws EvalException { if (!this.containsKey(key)) { throw new EvalException(loc, Printer.format("key %r not found in dictionary", key)); } return this.get(key); } @Override public final boolean containsKey(Object key, Location loc) throws EvalException { return this.containsKey(key); } public static SkylarkDict plus( SkylarkDict left, SkylarkDict right, @Nullable Environment env) { SkylarkDict result = SkylarkDict.of(env); result.putAllUnsafe(left); result.putAllUnsafe(right); return result; } }