Repository: mitsuhiko/minijinja
Pull request #724 (2025-02-12 19:54:00)

Provide a method to list the known variables
This adds `State::known_variables`.

Refs #715 and #720


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8ca4b60..ffa4a9b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -25,6 +25,8 @@ All notable changes to MiniJinja are documented here.
   objects in if-conditions and filters.  #719
 - Fixed a bug where `}}` caused a syntax error in expressions with
   open parentheses, braces or brackets.  #723
+- Added `State::known_variables` to return a list of known variables
+  and `Environment::globals`.  #724
 
 ## 2.8.0
 
diff --git a/minijinja-py/src/environment.rs b/minijinja-py/src/environment.rs
index 4aba832..095c40c 100644
--- a/minijinja-py/src/environment.rs
+++ b/minijinja-py/src/environment.rs
@@ -294,6 +294,16 @@ impl Environment {
         Ok(())
     }
 
+    /// The set of known global variables.
+    #[getter]
+    pub fn globals(&self, py: Python<'_>) -> PyResult<PyObject> {
+        let rv = PyDict::new(py);
+        for (key, value) in self.inner.lock().unwrap().env.globals() {
+            rv.set_item(key, to_python_value(value)?)?;
+        }
+        Ok(rv.into())
+    }
+
     /// Sets an auto escape callback.
     ///
     /// Note that because this interface in MiniJinja is infallible, the callback is
diff --git a/minijinja/src/environment.rs b/minijinja/src/environment.rs
index ead933a..75833b9 100644
--- a/minijinja/src/environment.rs
+++ b/minijinja/src/environment.rs
@@ -770,12 +770,19 @@ impl<'source> Environment<'source> {
         self.globals.remove(name);
     }
 
+    /// Returns an iterator of all global variables.
+    pub fn globals(&self) -> impl Iterator<Item = (&str, Value)> {
+        self.globals
+            .iter()
+            .map(|(key, value)| (key as &str, value.clone()))
+    }
+
     /// Returns an empty [`State`] for testing purposes and similar.
     pub fn empty_state(&self) -> State<'_, '_> {
         State::new_for_env(self)
     }
 
-    /// Looks up a function.
+    /// Looks up a global.
     pub(crate) fn get_global(&self, name: &str) -> Option<Value> {
         self.globals.get(name).cloned()
     }
diff --git a/minijinja/src/template.rs b/minijinja/src/template.rs
index e9cb668..d8434dd 100644
--- a/minijinja/src/template.rs
+++ b/minijinja/src/template.rs
@@ -251,6 +251,10 @@ impl<'env, 'source> Template<'env, 'source> {
     /// let undeclared = tmpl.undeclared_variables(true);
     /// // returns ["foo", "bar.baz"]
     /// ```
+    ///
+    /// Note that this does not special case global variables.  This means
+    /// that for instance a template that uses `namespace()` will return
+    /// `namespace` in the return value.
     pub fn undeclared_variables(&self, nested: bool) -> HashSet<String> {
         match parse(
             self.compiled.instructions.source(),
diff --git a/minijinja/src/vm/context.rs b/minijinja/src/vm/context.rs
index 24b642a..919ac40 100644
--- a/minijinja/src/vm/context.rs
+++ b/minijinja/src/vm/context.rs
@@ -137,46 +137,14 @@ pub(crate) struct Context<'env> {
 
 impl fmt::Debug for Context<'_> {
     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        fn dump<'a>(
-            m: &mut std::fmt::DebugMap,
-            seen: &mut HashSet<Cow<'a, str>>,
-            ctx: &'a Context<'a>,
-        ) -> fmt::Result {
-            for frame in ctx.stack.iter().rev() {
-                for (key, value) in frame.locals.iter() {
-                    if !seen.contains(&Cow::Borrowed(*key)) {
-                        m.entry(&key, value);
-                        seen.insert(Cow::Borrowed(key));
-                    }
-                }
-
-                if let Some(ref l) = frame.current_loop {
-                    if l.with_loop_var && !seen.contains("loop") {
-                        m.entry(&"loop", &l.object);
-                        seen.insert(Cow::Borrowed("loop"));
-                    }
-                }
-
-                if let Ok(iter) = frame.ctx.try_iter() {
-                    for key in iter {
-                        if let Some(str_key) = key.as_str() {
-                            if !seen.contains(&Cow::Borrowed(str_key)) {
-                                if let Ok(value) = frame.ctx.get_item(&key) {
-                                    m.entry(&str_key, &value);
-                                    seen.insert(Cow::Owned(str_key.to_owned()));
-                                }
-                            }
-                        }
-                    }
-                }
-            }
-            Ok(())
-        }
-
-        let mut m = f.debug_map();
-        let mut seen = HashSet::new();
-        ok!(dump(&mut m, &mut seen, self));
-        m.finish()
+        let mut vars = Vec::from_iter(self.known_variables(false));
+        vars.sort();
+        f.debug_map()
+            .entries(vars.into_iter().map(|key| {
+                let value = self.load(&key).unwrap_or_default();
+                (key, value)
+            }))
+            .finish()
     }
 }
 
@@ -287,6 +255,40 @@ impl<'env> Context<'env> {
         self.env.get_global(key)
     }
 
+    /// Returns an iterable of all declared variables.
+    pub fn known_variables(&self, with_globals: bool) -> HashSet<Cow<'_, str>> {
+        let mut seen = HashSet::<Cow<'_, str>>::new();
+        for frame in self.stack.iter().rev() {
+            for key in frame.locals.keys() {
+                if !seen.contains(&Cow::Borrowed(*key)) {
+                    seen.insert(Cow::Borrowed(key));
+                }
+            }
+
+            if let Some(ref l) = frame.current_loop {
+                if l.with_loop_var && !seen.contains("loop") {
+                    seen.insert(Cow::Borrowed("loop"));
+                }
+            }
+
+            if let Ok(iter) = frame.ctx.try_iter() {
+                for key in iter {
+                    if let Some(str_key) = key.as_str() {
+                        if !seen.contains(&Cow::Borrowed(str_key))
+                            && frame.ctx.get_item(&key).is_ok()
+                        {
+                            seen.insert(Cow::Owned(str_key.to_owned()));
+                        }
+                    }
+                }
+            }
+        }
+        if with_globals {
+            seen.extend(self.env.globals().map(|x| Cow::Borrowed(x.0)));
+        }
+        seen
+    }
+
     /// Pushes a new layer.
     pub fn push_frame(&mut self, layer: Frame<'env>) -> Result<(), Error> {
         ok!(self.check_depth());
diff --git a/minijinja/src/vm/state.rs b/minijinja/src/vm/state.rs
index 891324d..ab3e099 100644
--- a/minijinja/src/vm/state.rs
+++ b/minijinja/src/vm/state.rs
@@ -1,3 +1,4 @@
+use std::borrow::Cow;
 use std::collections::{BTreeMap, BTreeSet};
 use std::fmt;
 use std::sync::{Arc, Mutex};
@@ -224,6 +225,18 @@ impl<'template, 'env> State<'template, 'env> {
         self.ctx.exports().keys().copied().collect()
     }
 
+    /// Returns a list of all known variables.
+    ///
+    /// This list contains all variables that are currently known to the state.
+    /// To retrieve the values you can use [`lookup`](Self::lookup).  This will
+    /// include all the globals of the environment.  Note that if the context
+    /// has been initialized with an object that lies about variables (eg: it
+    /// does not correctly implement enumeration), the returned list might not
+    /// be complete.
+    pub fn known_variables(&self) -> Vec<Cow<'_, str>> {
+        Vec::from_iter(self.ctx.known_variables(true))
+    }
+
     /// Fetches a template by name with path joining.
     ///
     /// This works like [`Environment::get_template`] with the difference that the lookup


Test patch:
diff --git a/minijinja/tests/snapshots/test_templates__vm@debug.txt.snap b/minijinja/tests/snapshots/test_templates__vm@debug.txt.snap
index 5777728..adb47ce 100644
--- a/minijinja/tests/snapshots/test_templates__vm@debug.txt.snap
+++ b/minijinja/tests/snapshots/test_templates__vm@debug.txt.snap
@@ -10,17 +10,11 @@ State {
     current_block: None,
     auto_escape: None,
     ctx: {
-        "x": 0,
-        "loop": Loop {
-            len: Some(
-                1,
-            ),
-            idx: 0,
-            depth: 0,
-        },
         "f": minijinja::functions::builtins::range,
+        "loop": <loop 0/1>,
         "one_shot_iterator": <iterator>,
         "upper": 1,
+        "x": 0,
     },
     env: Environment {
         globals: {
diff --git a/minijinja/tests/test_environment.rs b/minijinja/tests/test_environment.rs
index 52d26d1..24dda79 100644
--- a/minijinja/tests/test_environment.rs
+++ b/minijinja/tests/test_environment.rs
@@ -96,6 +96,10 @@ fn test_globals() {
     env.add_template("test", "{{ a }}").unwrap();
     let tmpl = env.get_template("test").unwrap();
     assert_eq!(tmpl.render(()).unwrap(), "42");
+    assert_eq!(
+        env.globals().map(|x| x.0).collect::<Vec<_>>(),
+        vec!["a", "debug", "dict", "namespace", "range"]
+    );
 }
 
 #[test]
diff --git a/minijinja/tests/test_state.rs b/minijinja/tests/test_state.rs
index ffa5ef9..dbc9331 100644
--- a/minijinja/tests/test_state.rs
+++ b/minijinja/tests/test_state.rs
@@ -61,3 +61,13 @@ fn test_state_object_temps() {
         .unwrap();
     assert_eq!(rv, "1|2|3");
 }
+
+#[test]
+fn test_known_variables() {
+    let mut env = Environment::new();
+    env.add_global("foo", 42);
+    let state = env.empty_state();
+    let mut vars = state.known_variables();
+    vars.sort();
+    assert_eq!(vars, vec!["debug", "dict", "foo", "namespace", "range"]);
+}