Index: includes/ajax.inc
===================================================================
RCS file: /cvs/drupal/drupal/includes/ajax.inc,v
retrieving revision 1.19
diff -u -p -r1.19 ajax.inc
--- includes/ajax.inc	22 Nov 2009 02:48:37 -0000	1.19
+++ includes/ajax.inc	27 Dec 2009 01:43:41 -0000
@@ -262,30 +262,13 @@ function ajax_form_callback() {
   // drupal_process_form() set up.
   $form = drupal_rebuild_form($form_id, $form_state, $form_build_id);
 
-  // $triggering_element_path in a simple form might just be 'myselect', which
-  // would mean we should use the element $form['myselect']. For nested form
-  // elements we need to recurse into the form structure to find the triggering
-  // element, so we can retrieve the #ajax['callback'] from it.
-  if (!empty($triggering_element_path)) {
-    if (!isset($form['#access']) || $form['#access']) {
-      $triggering_element = $form;
-      foreach (explode('/', $triggering_element_path) as $key) {
-        if (!empty($triggering_element[$key]) && (!isset($triggering_element[$key]['#access']) || $triggering_element[$key]['#access'])) {
-          $triggering_element = $triggering_element[$key];
-        }
-        else {
-          // We did not find the $triggering_element or do not have #access,
-          // so break out and do not provide it.
-          $triggering_element = NULL;
-          break;
-        }
-      }
-    }
-  }
+  // Retrieve the triggering element from $form. If not found, default to
+  // $form_state['clicked_button'].
+  $triggering_element = drupal_subtree($form, $triggering_element_path, TRUE);
   if (empty($triggering_element)) {
     $triggering_element = $form_state['clicked_button'];
   }
-  // Now that we have the element, get a callback if there is one.
+  // Invoke the callback function specified by the triggering element.
   if (!empty($triggering_element)) {
     $callback = $triggering_element['#ajax']['callback'];
   }
@@ -418,7 +401,7 @@ function ajax_process_form($element, &$f
       'method'   => empty($element['#ajax']['method']) ? 'replace' : $element['#ajax']['method'],
       'progress' => empty($element['#ajax']['progress']) ? array('type' => 'throbber') : $element['#ajax']['progress'],
       'button'   => isset($element['#executes_submit_callback']) ? array($element['#name'] => $element['#value']) : FALSE,
-      'formPath' => implode('/', $element['#array_parents']),
+      'formPath' => drupal_tree_path($element['#array_parents'], DRUPAL_TREE_PATH_SLASH_DELIMITED),
     );
 
     // Convert a simple #ajax['progress'] type string into an array.
Index: includes/common.inc
===================================================================
RCS file: /cvs/drupal/drupal/includes/common.inc,v
retrieving revision 1.1070
diff -u -p -r1.1070 common.inc
--- includes/common.inc	26 Dec 2009 16:50:08 -0000	1.1070
+++ includes/common.inc	27 Dec 2009 01:43:41 -0000
@@ -167,6 +167,29 @@ define('DRUPAL_CACHE_PER_PAGE', 0x0004);
 define('DRUPAL_CACHE_GLOBAL', 0x0008);
 
 /**
+ * Constants defining the representation format of a tree path.
+ *
+ * A tree is a nested array used for structuring hierarchical data. A tree path
+ * identifies a section of the hierarchy. Given a tree path, a subtree of the
+ * original tree can be retrieved. For example, given $tree, the subtree
+ * $tree['a']['b']['c'] can be identified using one of the following:
+ * - array('a', 'b', 'c'): A tree path in array format, convenient for tree
+ *   traversal, because the array can be iterated. The #parents property of form
+ *   elements uses this format.
+ * - 'a/b/c': A tree path in slash delimited format, commonly used when needing
+ *   a string representation of a path (for example, for URLs).
+ * - 'a[b][c]': A tree path in brackets delimited format. This format is used
+ *   for constructing the 'name' attribute for input elements of a form, because
+ *   PHP automatically builds the $_POST tree from input element names that use
+ *   this format.
+ *
+ * @see drupal_tree_path()
+ */
+define('DRUPAL_TREE_PATH_ARRAY', 0);
+define('DRUPAL_TREE_PATH_SLASH_DELIMITED', 1);
+define('DRUPAL_TREE_PATH_BRACKETS_DELIMITED', 2);
+
+/**
  * Add content to a specified region.
  *
  * @param $region
@@ -6543,3 +6566,153 @@ function drupal_get_updaters() {
   }
   return $updaters;
 }
+
+/**
+ * Converts a tree path to the desired format.
+ *
+ * @param $path
+ *   A tree path in any format.
+ * @param $format
+ *   The desired format. One of:
+ *   - DRUPAL_TREE_PATH_ARRAY: (default) Return the path in array format. For
+ *     example: array('a', 'b', 'c').
+ *   - DRUPAL_TREE_PATH_SLASH_DELIMITED: Return the path in slash delimited
+ *     format. For example: 'a/b/c'.
+ *   - DRUPAL_TREE_PATH_BRACKETS_DELIMITED: Return the path in brackets
+ *     delimited format. For example: 'a[b][c]'.
+ * @return
+ *   The tree path in the desired format.
+ *
+ * @todo What's the best way to cross-link this to the PHPDoc above where these
+ *   constants are defined?
+ */
+function drupal_tree_path($path, $format = DRUPAL_TREE_PATH_ARRAY) {
+  // Normalize the path to array format. If passed an array, just ensure numeric
+  // and ordered keys. Otherwise, explode on '/' and '[' delimiters, and remove
+  // ']' pseudo-delimiters.
+  $path = is_array($path) ? array_values($path) : explode('/', strtr($path, array('[' => '/', ']' => '')));
+
+  // Generate the return value based on the desired format.
+  switch ($format) {
+    case DRUPAL_TREE_PATH_ARRAY:
+      $return = $path;
+      break;
+    case DRUPAL_TREE_PATH_SLASH_DELIMITED:
+      $return = implode('/', $path);
+      break;
+    case DRUPAL_TREE_PATH_BRACKETS_DELIMITED:
+      $return = !empty($path) ? $path[0] : '';
+      if (count($path) > 1) {
+        $return .= '[' . implode('][', array_slice($path, 1)) . ']';
+      }
+      break;
+  }
+
+  return $return;
+}
+
+/**
+ * Returns if two tree paths refer to the same path.
+ */
+function drupal_tree_paths_are_equal($path1, $path2) {
+  return (drupal_tree_path($path1) === drupal_tree_path($path2));
+}
+
+/**
+ * Returns if the first path is a descendant of the second path.
+ */
+function drupal_tree_path_is_descendant($descendant_candidate, $ancestor_candidate) {
+  $descendant_candidate = drupal_tree_path($descendant_candidate);
+  $ancestor_candidate = drupal_tree_path($ancestor_candidate);
+  return (count($descendant_candidate) > count($ancestor_candidate) && array_slice($descendant_candidate, 0, count($ancestor_candidate)) === $ancestor_candidate);
+}
+
+/**
+ * Returns the subtree of $tree identified by $path.
+ *
+ * If the code calling this function wants to retrieve a subtree in order to
+ * modify it, and have those modifications be reflected in the original tree,
+ * it needs to assign the result of this function by reference. Otherwise, if
+ * the code calling this function just wants a subtree for internal purposes, it
+ * does not need to assign the result by reference. Examples:
+ *
+ * @code
+ * function form_set_value($element, $value, &$form_state) {
+ *   // Assign the result by reference, so that setting its value updates
+ *   // the data within $form_state['values'].
+ *   $subtree = &drupal_subtree($form_state['values'], $element['#parents'], FALSE, TRUE);
+ *   $subtree = $value;
+ * }
+ * function field_add_more_js($form, $form_state) {
+ *   ...
+ *   // No need to assign result by reference, because we don't need to and
+ *   // don't want to modify anything within $form.
+ *   $field_form = drupal_subtree($form, $form['#fields'][$field_name]['form_path']);
+ *   ...
+ *   return drupal_render($field_form);
+ * }
+ * @endcode
+ *
+ * @param $tree
+ *   The root tree from which a subtree is desired.
+ * @param $path
+ *   The path identifying which subtree is desired.
+ * @param $check_access
+ *   (optional) Boolean whether a #access of FALSE within any of the subarrays
+ *   along the path should result in NULL being returned. This should be set to
+ *   TRUE when called for the purpose of retrieving an element from within a
+ *   Form API array or render array if it's desired to only retrieve an element
+ *   that the user has access to.
+ * @param $create
+ *   (optional) Boolean whether keys specified by $path should be added to the
+ *   tree if they don't already exist. This is used when wanting to add to the
+ *   tree (for an example, @see form_set_value()).
+ * @return
+ *   A reference to the subtree (which could have a NULL value but still be a
+ *   valid subtree), or a NULL that is not a reference to anything if the
+ *   subtree doesn't exist or access to it is denied.
+ *
+ * @see drupal_tree_path()
+ */
+function &drupal_subtree(&$tree, $path, $check_access = FALSE, $create = FALSE) {
+  // If checking access, then it must be checked for the tree as well as each
+  // step along the path.
+  if ($check_access && isset($tree['#access']) && !$tree['#access']) {
+    $subtree = NULL;
+  }
+  else {
+    $subtree = &$tree;
+    foreach (drupal_tree_path($path) as $step) {
+      // If this step doesn't exist, then either create it, or break out of the
+      // loop and return NULL.
+      if (!is_array($subtree) || !array_key_exists($step, $subtree)) {
+        if ($create) {
+          if (!is_array($subtree)) {
+            $subtree = array();
+          }
+          // Create new entries as NULL. If this is the last step, NULL is the
+          // desired value for the new entry. If this is not the last step, then
+          // it will be modified to an array in the next step.
+          $subtree[$step] = NULL;
+        }
+        else {
+          // Break the reference before setting it to NULL.
+          unset($subtree);
+          $subtree = NULL;
+          break;
+        }
+      }
+      // Traverse the step.
+      $subtree = &$subtree[$step];
+      // If checking access, and #access is set and is FALSE, break out of the
+      // loop and return NULL.
+      if ($check_access && isset($subtree['#access']) && !$subtree['#access']) {
+        // Break the reference before setting it to NULL.
+        unset($subtree);
+        $subtree = NULL;
+        break;
+      }
+    }
+  }
+  return $subtree;
+}
Index: includes/form.inc
===================================================================
RCS file: /cvs/drupal/drupal/includes/form.inc,v
retrieving revision 1.421
diff -u -p -r1.421 form.inc
--- includes/form.inc	17 Dec 2009 21:59:31 -0000	1.421
+++ includes/form.inc	27 Dec 2009 01:43:41 -0000
@@ -897,7 +897,7 @@ function _form_validate(&$elements, &$fo
       // length if it's a string, and the item count if it's an array.
       // An unchecked checkbox has a #value of numeric 0, different than string
       // '0', which could be a valid value.
-      if ($elements['#required'] && (!count($elements['#value']) || (is_string($elements['#value']) && strlen(trim($elements['#value'])) == 0) || $elements['#value'] === 0)) {
+      if ($elements['#required'] && !_form_validate_skip($elements, $form_state) && (!count($elements['#value']) || (is_string($elements['#value']) && strlen(trim($elements['#value'])) == 0) || $elements['#value'] === 0)) {
         form_error($elements, $t('!name field is required.', array('!name' => $elements['#title'])));
       }
 
@@ -935,7 +935,7 @@ function _form_validate(&$elements, &$fo
     }
     // Call any element-specific validators. These must act on the element
     // #value data.
-    elseif (isset($elements['#element_validate'])) {
+    elseif (isset($elements['#element_validate']) && !_form_validate_skip($elements, $form_state)) {
       foreach ($elements['#element_validate'] as $function) {
         if (function_exists($function))  {
           $function($elements, $form_state, $form_state['complete form']);
@@ -947,6 +947,85 @@ function _form_validate(&$elements, &$fo
 }
 
 /**
+ * Check if an element's validation may be skipped.
+ *
+ * A form button may specify the #validate_partial property to limit form
+ * validation to sections of $form_state['values']. Such buttons are required to
+ * also define custom #submit handlers, which ONLY process form values within
+ * those sections.
+ *
+ * For example, when an "add another item" button for a multiple value field is
+ * clicked, we don't want a not yet filled out but required title to cause a
+ * validation error, preventing a new item from being added to the form. By
+ * setting that button's #validate_partial property, and only including
+ * the field's $field_name within #validate_partial['sections'], only elements
+ * within the field will be validated when the button is clicked, and the button
+ * submit handler can assume that data within $form_state['values'][$field_name]
+ * is valid, but must not assume that any other part of $form_state['values'] is
+ * valid.
+ *
+ * This will never skip properties that are enforced by HTML, such as #maxlength
+ * or #options.
+ *
+ * @param $element
+ *   The element whose #parents will be checked to determine if this element's
+ *   validation should be skipped.
+ * @param $form_state
+ *   A keyed array containing the current state of the form. The
+ *   'clicked_button' property is checked to determine which elements should be
+ *   validated.
+ *
+ * @return
+ *   Boolean TRUE if the element should not be validated. FALSE if the element
+ *   should be validated.
+ *
+ * @todo D8: This should live in _form_builder_handle_input_element(), because
+ *   ideally, we want to skip not just validation, but all input processing for
+ *   unrelated elements. However, we currently cannot skip input processing,
+ *   because node and field validate/submit handlers cast $form_state['values']
+ *   into an object and presume that doing so results in an object containing
+ *   most properties of an entity.
+ *
+ * @todo The #validate_partial property needs to be documented somewhere other
+ *   than with this internal function. The documentation should include examples
+ *   of different use-cases for 'sections' and 'skip_ancestors'. It would make
+ *   sense for this to live with wherever #validate and #element_validate are
+ *   documented. Where is that?
+ */
+function _form_validate_skip($element, $form_state) {
+  // Check whether the pressed button defines #validate_partial to limit
+  // validation. We prevent insecure execution of form-level submit handlers by
+  // not supporting partial validation unless a button-level submit handler is
+  // defined.
+  if (!(isset($form_state['clicked_button']['#validate_partial']) && isset($form_state['clicked_button']['#submit']))) {
+    return FALSE;
+  }
+  // Iterate over #validate_partial['sections'], and if $element is responsible
+  // for data within that section, it needs validation.
+  $validate = FALSE;
+  foreach ($form_state['clicked_button']['#validate_partial']['sections'] as $section) {
+    // Check if the element's #parents is such that the element's values are
+    // within the section.
+    if (drupal_tree_paths_are_equal($element['#parents'], $section) || drupal_tree_path_is_descendant($element['#parents'], $section)) {
+      $validate = TRUE;
+      break;
+    }
+    // If an element's values occupy a part of $form_state['values'] that is
+    // an ancestor of the section, then its validation handlers may perform
+    // additional validation on data within the section, and should run by
+    // default. This includes the top-level form which is an ancestor to
+    // everything. #validate_partial['skip_ancestors'] may be set to skip
+    // ancestor element and form-level validation handlers.
+    if (empty($form_state['clicked_button']['#validate_partial']['skip_ancestors']) && drupal_tree_path_is_descendant($section, $element['#parents'])) {
+      $validate = TRUE;
+      break;
+    }
+  }
+  $skip = !$validate;
+  return $skip;
+}
+
+/**
  * A helper function used to execute custom validation and submission
  * handlers for a given form. Button-specific handlers are checked
  * first. If none exist, the function falls back to form-level handlers.
@@ -969,7 +1048,13 @@ function form_execute_handlers($type, &$
   }
   // Otherwise, check for a form-level handler.
   elseif (isset($form['#' . $type])) {
-    $handlers = $form['#' . $type];
+    // Check if form-level validation handlers should be skipped.
+    if ($type == 'validate' && _form_validate_skip($form, $form_state)) {
+      $handlers = array();
+    }
+    else {
+      $handlers = $form['#' . $type];
+    }
   }
   else {
     $handlers = array();
@@ -1056,6 +1141,11 @@ function form_get_error($element) {
  * Flag an element as having an error.
  */
 function form_error(&$element, $message = '') {
+  // @todo If a form uses #validate_partial without setting 'skip_ancestors',
+  //   it's possible that an ancestor validation handler calls form_error() for
+  //   an element that isn't within 'sections'. It may be desirable to add a
+  //   _form_validate_skip() check here to ignore those errors, but this
+  //   requires access to the $form_state variable.
   form_set_error(implode('][', $element['#parents']), $message);
 }
 
@@ -1211,18 +1301,15 @@ function form_builder($form_id, $element
  */
 function _form_builder_handle_input_element($form_id, &$element, &$form_state) {
   if (!isset($element['#name'])) {
-    $name = array_shift($element['#parents']);
-    $element['#name'] = $name;
     if ($element['#type'] == 'file') {
       // To make it easier to handle $_FILES in file.inc, we place all
       // file fields in the 'files' array. Also, we do not support
       // nested file names.
-      $element['#name'] = 'files[' . $element['#name'] . ']';
+      $element['#name'] = 'files[' . $element['#parents'][0] . ']';
     }
-    elseif (count($element['#parents'])) {
-      $element['#name'] .= '[' . implode('][', $element['#parents']) . ']';
+    else {
+      $element['#name'] = drupal_tree_path($element['#parents'], DRUPAL_TREE_PATH_BRACKETS_DELIMITED);
     }
-    array_unshift($element['#parents'], $name);
   }
 
   if (!empty($element['#disabled'])) {
@@ -1236,17 +1323,16 @@ function _form_builder_handle_input_elem
     if ($form_state['programmed'] || ($form_state['process_input'] && (!isset($element['#access']) || $element['#access']))) {
       // Get the input for the current element. NULL values in the input need to
       // be explicitly distinguished from missing input. (see below)
-      $input = $form_state['input'];
-      $input_exists = TRUE;
-      foreach ($element['#parents'] as $parent) {
-        if (is_array($input) && array_key_exists($parent, $input)) {
-          $input = $input[$parent];
-        }
-        else {
-          $input = NULL;
-          $input_exists = FALSE;
-          break;
-        }
+      $parents = $element['#parents'];
+      $last_parent = array_pop($parents);
+      $input = drupal_subtree($form_state['input'], $parents);
+      if (is_array($input) && array_key_exists($last_parent, $input)) {
+        $input = $input[$last_parent];
+        $input_exists = TRUE;
+      }
+      else {
+        $input = NULL;
+        $input_exists = FALSE;
       }
       // For browser-submitted forms, the submitted values do not contain values
       // for certain elements (empty multiple select, unchecked checkbox).
@@ -1258,11 +1344,8 @@ function _form_builder_handle_input_elem
       // submit explicit NULL values when calling drupal_form_submit(), so we do
       // not modify $form_state['input'] for them.
       if (!$input_exists && !$form_state['rebuild'] && !$form_state['programmed']) {
-        // We leverage the internal logic of form_set_value() to change the
-        // input values by passing $form_state['input'] instead of the usual
-        // $form_state['values']. In effect, this adds the necessary parent keys
-        // to $form_state['input'] and sets the element's input value to NULL.
-        _form_set_value($form_state['input'], $element, $element['#parents'], NULL);
+        // Add the necessary parent keys to $form_state['input'].
+        drupal_subtree($form_state['input'], $element['#parents'], FALSE, TRUE);
         $input_exists = TRUE;
       }
       // If we have input for the current element, assign it to the #value
@@ -1410,31 +1493,10 @@ function form_state_values_clean(&$form_
   // type. We remove the button value separately for each button element.
   foreach ($form_state['buttons'] as $button_type => $buttons) {
     foreach ($buttons as $button) {
-      // Remove this button's value from the submitted form values by finding
-      // the value corresponding to this button.
-      // We iterate over the #parents of this button and move a reference to
-      // each parent in $form_state['values']. For example, if #parents is:
-      //   array('foo', 'bar', 'baz')
-      // then the corresponding $form_state['values'] part will look like this:
-      // array(
-      //   'foo' => array(
-      //     'bar' => array(
-      //       'baz' => 'button_value',
-      //     ),
-      //   ),
-      // )
-      // We start by (re)moving 'baz' to $last_parent, so we are able unset it
-      // at the end of the iteration. Initially, $values will contain a
-      // reference to $form_state['values'], but in the iteration we move the
-      // reference to $form_state['values']['foo'], and finally to
-      // $form_state['values']['foo']['bar'], which is the level where we can
-      // unset 'baz' (that is stored in $last_parent).
+      // Remove this button's value from the submitted form values.
       $parents = $button['#parents'];
-      $values = &$form_state['values'];
       $last_parent = array_pop($parents);
-      foreach ($parents as $parent) {
-        $values = &$values[$parent];
-      }
+      $values = &drupal_subtree($form_state['values'], $parents);
       unset($values[$last_parent]);
     }
   }
@@ -1651,26 +1713,8 @@ function form_type_token_value($element,
  *   Form state array where the value change should be recorded.
  */
 function form_set_value($element, $value, &$form_state) {
-  _form_set_value($form_state['values'], $element, $element['#parents'], $value);
-}
-
-/**
- * Helper function for form_set_value() and _form_builder_handle_input_element().
- *
- * We iterate over $parents and create nested arrays for them in $form_values if
- * needed. Then we insert the value into the last parent key.
- */
-function _form_set_value(&$form_values, $element, $parents, $value) {
-  $parent = array_shift($parents);
-  if (empty($parents)) {
-    $form_values[$parent] = $value;
-  }
-  else {
-    if (!isset($form_values[$parent])) {
-      $form_values[$parent] = array();
-    }
-    _form_set_value($form_values[$parent], $element, $parents, $value);
-  }
+  $subtree = &drupal_subtree($form_state['values'], $element['#parents'], FALSE, TRUE);
+  $subtree = $value;
 }
 
 function form_options_flatten($array, $reset = TRUE) {
Index: modules/field/field.form.inc
===================================================================
RCS file: /cvs/drupal/drupal/modules/field/field.form.inc,v
retrieving revision 1.38
diff -u -p -r1.38 field.form.inc
--- modules/field/field.form.inc	21 Dec 2009 13:47:32 -0000	1.38
+++ modules/field/field.form.inc	27 Dec 2009 01:43:42 -0000
@@ -214,7 +214,10 @@ function field_multiple_value_form($fiel
           '#name' => $field_name . '_add_more',
           '#value' => t('Add another item'),
           '#attributes' => array('class' => array('field-add-more-submit')),
-          // Submit callback for disabled JavaScript.
+          '#validate_partial' => array(
+            'sections' => array($field_name),
+            'skip_ancestors' => TRUE,
+          ),
           '#submit' => array('field_add_more_submit'),
           '#ajax' => array(
             'callback' => 'field_add_more_js',
@@ -369,13 +372,8 @@ function field_add_more_js($form, $form_
   if ($field['cardinality'] != FIELD_CARDINALITY_UNLIMITED) {
     ajax_render(array());
   }
-  // Navigate to the right part of the form.
-  $form_path = $form['#fields'][$field_name]['form_path'];
-  $field_form = $form;
-  foreach ($form_path as $key) {
-    $field_form = $field_form[$key];
-  }
-
+  // Retrieve just the field's part of the form.
+  $field_form = drupal_subtree($form, $form['#fields'][$field_name]['form_path']);
   // Add a DIV around the new field to receive the AJAX effect.
   $langcode = $field_form['#language'];
   $delta = $field_form[$langcode]['#max_delta'];
Index: modules/file/file.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/file/file.module,v
retrieving revision 1.15
diff -u -p -r1.15 file.module
--- modules/file/file.module	12 Dec 2009 23:04:57 -0000	1.15
+++ modules/file/file.module	27 Dec 2009 01:43:42 -0000
@@ -468,9 +468,9 @@ function file_managed_file_value(&$eleme
   $fid = 0;
 
   // Find the current value of this field from the form state.
-  $form_state_fid = $form_state['values'];
-  foreach ($element['#parents'] as $parent) {
-    $form_state_fid = isset($form_state_fid[$parent]) ? $form_state_fid[$parent] : 0;
+  $form_state_fid = drupal_subtree($form_state['values'], $element['#parents']);
+  if (!isset($form_state_fid)) {
+    $form_state_fid = 0;
   }
 
   if ($element['#extended'] && isset($form_state_fid['fid'])) {
Index: modules/poll/poll.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/poll/poll.module,v
retrieving revision 1.330
diff -u -p -r1.330 poll.module
--- modules/poll/poll.module	26 Dec 2009 16:50:09 -0000	1.330
+++ modules/poll/poll.module	27 Dec 2009 01:43:42 -0000
@@ -273,7 +273,13 @@ function poll_form($node, &$form_state) 
     '#value' => t('More choices'),
     '#description' => t("If the amount of boxes above isn't enough, click here to add more choices."),
     '#weight' => 1,
-    '#submit' => array('poll_more_choices_submit'), // If no javascript action.
+    // @todo choice_wrapper has #tree set to FALSE, so this doesn't work.
+    //   @see _form_validate_skip().
+    '#validate_partial' => array(
+      'sections' => array('choice_wrapper'),
+      'skip_ancestors' => TRUE,
+    ),
+    '#submit' => array('poll_more_choices_submit'),
     '#ajax' => array(
       'callback' => 'poll_choice_js',
       'wrapper' => 'poll-choices',
Index: modules/simpletest/tests/form.test
===================================================================
RCS file: /cvs/drupal/drupal/modules/simpletest/tests/form.test,v
retrieving revision 1.31
diff -u -p -r1.31 form.test
--- modules/simpletest/tests/form.test	17 Dec 2009 17:18:03 -0000	1.31
+++ modules/simpletest/tests/form.test	27 Dec 2009 01:43:42 -0000
@@ -231,6 +231,46 @@ class FormValidationTestCase extends Dru
     $this->assertNoFieldByName('name', t('Form element was hidden.'));
     $this->assertText('Name value: element_validate_access', t('Value for inaccessible form element exists.'));
   }
+
+  /**
+   * Tests partial form validation using #validate_partial.
+   */
+  function testValidatePartial() {
+    $this->drupalGet('form-test/validate-partial');
+
+    // Add one 'field' value without a value for required 'title' element.
+    $edit = array(
+      'field[new]' => 'foo',
+    );
+    $this->drupalPost(NULL, $edit, 'Add');
+    $this->assertText('Form element validation handler for field triggered.');
+    $this->assertFieldByName('field[0]', $edit['field[new]'], t('First field element contains submitted value.'));
+    $this->assertNoText(t('!name field is required.', array('!name' => 'Title')));
+    $this->assertNoText('Form element validation handler for element triggered.');
+    $this->assertNoText('Form submit handler triggered.');
+
+    // Try to submit the form without posting a title, but also not altering the
+    // default value for 'element' to trigger the element validation handler.
+    $this->drupalPost(NULL, array(), 'Submit');
+    $this->assertText(t('!name field is required.', array('!name' => 'Title')));
+    $this->assertText('Form element validation handler for element triggered.');
+    $this->assertNoText('Form submit handler triggered.');
+
+    // Try again without posting a title.
+    $edit = array(
+      'element' => 'bar',
+    );
+    $this->drupalPost(NULL, $edit, 'Submit');
+    $this->assertText(t('!name field is required.', array('!name' => 'Title')));
+    $this->assertNoText('Form submit handler triggered.');
+
+    // Now do a valid post and verify invocation of submit handler.
+    $edit = array(
+      'title' => 'beer',
+    );
+    $this->drupalPost(NULL, $edit, 'Submit');
+    $this->assertText('Form submit handler triggered.');
+  }
 }
 
 /**
Index: modules/simpletest/tests/form_test.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/simpletest/tests/form_test.module,v
retrieving revision 1.24
diff -u -p -r1.24 form_test.module
--- modules/simpletest/tests/form_test.module	17 Dec 2009 17:18:03 -0000	1.24
+++ modules/simpletest/tests/form_test.module	27 Dec 2009 01:43:42 -0000
@@ -17,6 +17,12 @@ function form_test_menu() {
     'access arguments' => array('access content'),
     'type' => MENU_CALLBACK,
   );
+  $items['form-test/validate-partial'] = array(
+    'title' => '#validate_partial test',
+    'page callback' => 'drupal_get_form',
+    'page arguments' => array('form_test_validate_partial_form'),
+    'access arguments' => array('access content'),
+  );
 
   $items['form_test/tableselect/multiple-true'] = array(
     'title' => 'Tableselect checkboxes test',
@@ -204,6 +210,96 @@ function form_test_validate_form_validat
 }
 
 /**
+ * Form builder for testing #validate_partial.
+ */
+function form_test_validate_partial_form($form, &$form_state) {
+  $form['#tree'] = TRUE;
+
+  $form['title'] = array(
+    '#type' => 'textfield',
+    '#title' => 'Title',
+    '#required' => TRUE,
+  );
+  $form['element'] = array(
+    '#type' => 'textfield',
+    '#title' => 'Element',
+    '#default_value' => 'ELEMENT',
+    '#element_validate' => array('form_test_validate_partial_form_element_validate'),
+  );
+
+  $form['field'] = array(
+    '#type' => 'fieldset',
+    '#title' => 'Field',
+  );
+  if (empty($form_state['storage']['field'])) {
+    $form_state['storage']['field'] = array();
+  }
+  // Always add an empty field the user can submit.
+  $form_state['storage']['field']['new'] = '';
+  $weight = 0;
+  foreach ($form_state['storage']['field'] as $delta => $value) {
+    $form['field'][$delta] = array(
+      '#type' => 'textfield',
+      '#title' => 'Field ' . $delta,
+      '#default_value' => $value,
+      '#element_validate' => array('form_test_validate_partial_form_field_validate'),
+      '#weight' => $weight++,
+    );
+  }
+  $form['field']['new']['#weight'] = 999;
+  $form['field']['submit'] = array(
+    '#type' => 'submit',
+    '#value' => 'Add',
+    '#validate_partial' => array(
+      'sections' => array('field'),
+      'skip_ancestors' => TRUE,
+    ),
+    '#submit' => array('form_test_validate_partial_form_field_submit'),
+    '#weight' => 1000,
+  );
+
+  $form['submit'] = array('#type' => 'submit', '#value' => 'Submit');
+
+  return $form;
+}
+
+/**
+ * Form element validation handler for 'element' in form_test_validate_partial_form().
+ */
+function form_test_validate_partial_form_element_validate($element, &$form_state) {
+  if (empty($form_state['values']['element']) || $form_state['values']['element'] == $element['#default_value']) {
+    form_error($element, 'Form element validation handler for element triggered.');
+  }
+}
+
+/**
+ * Form element validation handler for 'field' in form_test_validate_partial_form().
+ */
+function form_test_validate_partial_form_field_validate($element, &$form_state) {
+  drupal_set_message('Form element validation handler for field triggered.');
+}
+
+/**
+ * Form element submit handler for form_test_validate_partial_form().
+ */
+function form_test_validate_partial_form_field_submit($form, &$form_state) {
+  // Store the new value and clear out the 'new' field.
+  $form_state['storage']['field'][] = $form_state['values']['field']['new'];
+  unset($form_state['input']['field']['new']);
+  // @todo The form constructor should be able to define this, but currently
+  //   cannot. @see http://drupal.org/node/648170
+  $form_state['rebuild'] = TRUE;
+  $form_state['cache'] = TRUE;
+}
+
+/**
+ * Form submit handler for form_test_validate_partial_form().
+ */
+function form_test_validate_partial_form_submit($form, &$form_state) {
+  drupal_set_message('Form submit handler triggered.');
+}
+
+/**
  * Create a header and options array. Helper function for callbacks.
  */
 function _form_test_tableselect_get_data() {
