Index: django/newforms/forms.py
===================================================================
--- django/newforms/forms.py	(revision 5380)
+++ django/newforms/forms.py	(working copy)
@@ -9,7 +9,7 @@
 from django.utils.encoding import StrAndUnicode
 
 from fields import Field
-from widgets import TextInput, Textarea
+from widgets import Media, TextInput, Textarea
 from util import flatatt, ErrorDict, ErrorList, ValidationError
 
 __all__ = ('BaseForm', 'Form')
@@ -209,6 +209,16 @@
         """
         return self.cleaned_data
 
+    def _get_media(self):
+        """
+        Provide a description of all media required to render the widgets on this form
+        """
+        m = Media()
+        for field in self.fields.values():
+            m = m + field.widget.media
+        return m
+    media = property(_get_media)
+    
 class Form(BaseForm):
     "A collection of Fields, plus their associated data."
     # This is a separate class from BaseForm in order to abstract the way
Index: django/newforms/widgets.py
===================================================================
--- django/newforms/widgets.py	(revision 5380)
+++ django/newforms/widgets.py	(working copy)
@@ -8,6 +8,7 @@
     from sets import Set as set # Python 2.3 fallback
 from itertools import chain
 
+from django.conf import settings
 from django.utils.datastructures import MultiValueDict
 from django.utils.html import escape
 from django.utils.translation import gettext
@@ -16,14 +17,74 @@
 from util import flatatt
 
 __all__ = (
-    'Widget', 'TextInput', 'PasswordInput',
+    'Media', 'Widget', 'TextInput', 'PasswordInput',
     'HiddenInput', 'MultipleHiddenInput',
     'FileInput', 'Textarea', 'CheckboxInput',
     'Select', 'NullBooleanSelect', 'SelectMultiple', 'RadioSelect',
     'CheckboxSelectMultiple', 'MultiWidget', 'SplitDateTimeWidget',
 )
 
+MEDIA_TYPES = ('css','js')
+
+class Media(StrAndUnicode):
+    def __init__(self, media=None, **kwargs):
+        if media:
+            media_attrs = media.__dict__
+            del media_attrs['__module__']
+            del media_attrs['__doc__']
+        else:
+            media_attrs = kwargs
+            
+        for attr_name in MEDIA_TYPES:
+            setattr(self, '_' + attr_name, media_attrs.pop(attr_name, []))
+
+        # Any leftover attributes must be invalid.
+        if media_attrs != {}:
+            raise TypeError, "'class Media' has invalid attribute(s): %s" % ','.join(media_attrs.keys())
+        
+    def __unicode__(self):
+        return self.render()
+        
+    def render(self):
+        output = []
+        for name in MEDIA_TYPES:
+            for path in getattr(self, '_' + name):
+                output.append(getattr(self, 'render_' + name)(path))
+        return u'\n'.join(output)
+        
+    def render_js(self, path):
+        return u'<script type="text/javascript" src="%s" />' % self.absolute_path(path)
+        
+    def render_css(self, path):
+        return u'<link href="%s" type="text/css" rel="stylesheet" />' % self.absolute_path(path)
+
+    def absolute_path(self, path):
+        return path.startswith('http://') and path or u''.join([settings.MEDIA_URL,path])
+
+    def __getitem__(self, name):
+        "Returns a Media object that only contains media of the given type"
+        if name in MEDIA_TYPES:
+            return Media(**{name: getattr(self, '_' + name)})
+        raise KeyError('Unknown media type "%s"' % name)
+
+    def __add__(self, other):
+        "Combine two media objects to produce the union of all media"
+        combined = {}
+        for name in MEDIA_TYPES:
+            combined[name] = list(getattr(self, '_' + name, [])) + \
+                [m for m in getattr(other, '_' + name, []) if m not in getattr(self, '_' + name, []) ]
+        return Media(**combined)
+        
+class WidgetBase(type):
+    "Metaclass for all widgets"
+    def __new__(cls, name, bases, attrs):        
+        media = Media(attrs.pop('Media', None))
+        new_class = type.__new__(cls, name, bases, attrs)
+        setattr(new_class, 'media', media)
+        return new_class
+        
 class Widget(object):
+    __metaclass__ = WidgetBase
     is_hidden = False          # Determines whether this corresponds to an <input type="hidden">.
 
     def __init__(self, attrs=None):
Index: tests/regressiontests/forms/media.py
===================================================================
--- tests/regressiontests/forms/media.py	(revision 0)
+++ tests/regressiontests/forms/media.py	(revision 0)
@@ -0,0 +1,109 @@
+# -*- coding: utf-8 -*-
+# Tests for the media handling on widgets and forms
+
+media_tests = r"""
+>>> from django.newforms import TextInput, Media, TextInput, CharField, Form
+>>> from django.conf import settings
+>>> settings.MEDIA_URL = 'http://media.example.com'
+
+# A widget can exist without a media definition
+>>> class MyWidget(TextInput):
+...     pass
+
+>>> w = MyWidget()
+>>> print w.media
+<BLANKLINE>
+
+# A widget can define media if it needs to.
+# Any absolute path will be preserved; relative paths are combined
+# with the value of settings.MEDIA_URL
+>>> class MyWidget1(TextInput):
+...     class Media:
+...         css = ('/path/to/css1','/path/to/css2')
+...         js = ('/path/to/js1','http://media.other.com/path/to/js2')
+
+>>> w1 = MyWidget1()
+>>> print w1.media
+<link href="http://media.example.com/path/to/css1" type="text/css" rel="stylesheet" />
+<link href="http://media.example.com/path/to/css2" type="text/css" rel="stylesheet" />
+<script type="text/javascript" src="http://media.example.com/path/to/js1" />
+<script type="text/javascript" src="http://media.other.com/path/to/js2" />
+
+# Media objects can be interrogated by media type
+>>> print w1.media['css']
+<link href="http://media.example.com/path/to/css1" type="text/css" rel="stylesheet" />
+<link href="http://media.example.com/path/to/css2" type="text/css" rel="stylesheet" />
+
+>>> print w1.media['js']
+<script type="text/javascript" src="http://media.example.com/path/to/js1" />
+<script type="text/javascript" src="http://media.other.com/path/to/js2" />
+
+# Media objects can be combined. Any given media resource will appear only
+# once. Duplicated media definitions are ignored.
+>>> class MyWidget2(TextInput):
+...     class Media:
+...         css = ('/path/to/css2','/path/to/css3')
+...         js = ('/path/to/js1','/path/to/js3')
+
+>>> class MyWidget3(TextInput):
+...     class Media:
+...         css = ('/path/to/css3','/path/to/css1')
+...         js = ('/path/to/js1','/path/to/js3')
+
+>>> w2 = MyWidget2()
+>>> w3 = MyWidget3()
+>>> print w1.media + w2.media + w3.media
+<link href="http://media.example.com/path/to/css1" type="text/css" rel="stylesheet" />
+<link href="http://media.example.com/path/to/css2" type="text/css" rel="stylesheet" />
+<link href="http://media.example.com/path/to/css3" type="text/css" rel="stylesheet" />
+<script type="text/javascript" src="http://media.example.com/path/to/js1" />
+<script type="text/javascript" src="http://media.other.com/path/to/js2" />
+<script type="text/javascript" src="http://media.example.com/path/to/js3" />
+
+# If a widget extends another, media must be redefined
+>>> class MyWidget4(MyWidget1):
+...     pass
+
+>>> w4 = MyWidget4()
+>>> print w4.media
+<BLANKLINE>
+
+# If a widget extends another, media from the parent widget is ignored
+>>> class MyWidget5(MyWidget1):
+...     class Media:
+...         css = ('/path/to/css3','/path/to/css1')
+...         js = ('/path/to/js1','/path/to/js3')
+
+>>> w5 = MyWidget5()
+>>> print w5.media
+<link href="http://media.example.com/path/to/css3" type="text/css" rel="stylesheet" />
+<link href="http://media.example.com/path/to/css1" type="text/css" rel="stylesheet" />
+<script type="text/javascript" src="http://media.example.com/path/to/js1" />
+<script type="text/javascript" src="http://media.example.com/path/to/js3" />
+
+# You can ask a form for the media required by its widgets.
+>>> class MyForm(Form):
+...     field1 = CharField(max_length=20, widget=MyWidget1())
+...     field2 = CharField(max_length=20, widget=MyWidget2())
+>>> f1 = MyForm()
+>>> print f1.media
+<link href="http://media.example.com/path/to/css1" type="text/css" rel="stylesheet" />
+<link href="http://media.example.com/path/to/css2" type="text/css" rel="stylesheet" />
+<link href="http://media.example.com/path/to/css3" type="text/css" rel="stylesheet" />
+<script type="text/javascript" src="http://media.example.com/path/to/js1" />
+<script type="text/javascript" src="http://media.other.com/path/to/js2" />
+<script type="text/javascript" src="http://media.example.com/path/to/js3" />
+
+# Form media can be combined to produce a single media definition.
+>>> class AnotherForm(Form):
+...     field3 = CharField(max_length=20, widget=MyWidget3())
+>>> f2 = AnotherForm()
+>>> print f1.media + f2.media
+<link href="http://media.example.com/path/to/css1" type="text/css" rel="stylesheet" />
+<link href="http://media.example.com/path/to/css2" type="text/css" rel="stylesheet" />
+<link href="http://media.example.com/path/to/css3" type="text/css" rel="stylesheet" />
+<script type="text/javascript" src="http://media.example.com/path/to/js1" />
+<script type="text/javascript" src="http://media.other.com/path/to/js2" />
+<script type="text/javascript" src="http://media.example.com/path/to/js3" />
+
+"""
\ No newline at end of file
Index: tests/regressiontests/forms/tests.py
===================================================================
--- tests/regressiontests/forms/tests.py	(revision 5380)
+++ tests/regressiontests/forms/tests.py	(working copy)
@@ -1,6 +1,7 @@
 # -*- coding: utf-8 -*-
 from localflavor import localflavor_tests
 from regressions import regression_tests
+from media import media_tests
 
 form_tests = r"""
 >>> from django.newforms import *
@@ -3677,6 +3678,7 @@
 """
 
 __test__ = {
+    'media_tests': media_tests,
     'form_tests': form_tests,
     'localflavor': localflavor_tests,
     'regressions': regression_tests,
