[Pypi-checkins] r759 - in trunk/pypi: . templates tools

martin.von.loewis python-checkins at python.org
Sat Apr 24 12:22:22 CEST 2010


Author: martin.von.loewis
Date: Sat Apr 24 12:22:22 2010
New Revision: 759

Added:
   trunk/pypi/tools/sql-migrate-20100313.sql
      - copied unchanged from r758, branches/tarek-pep-345-support/tools/sql-migrate-20100313.sql
Modified:
   trunk/pypi/   (props changed)
   trunk/pypi/README
   trunk/pypi/config.ini
   trunk/pypi/pkgbase_schema.sql
   trunk/pypi/pypi.wsgi
   trunk/pypi/rpc.py
   trunk/pypi/store.py
   trunk/pypi/templates/display.pt
   trunk/pypi/templates/standard_template.pt
   trunk/pypi/tools/apache_count_dist.py   (props changed)
   trunk/pypi/tools/sql-migrate-20090327.sql   (props changed)
   trunk/pypi/webui.py
Log:
Merge tarek-pep-345-support.


Modified: trunk/pypi/README
==============================================================================
--- trunk/pypi/README	(original)
+++ trunk/pypi/README	Sat Apr 24 12:22:22 2010
@@ -1,13 +1,44 @@
-
 Required packages
-----------------------
+-----------------
+
+To run the PyPI software, you need PostgreSQL, and all
+these packages located at PyPI:
+
+- cElementTree
+- zope.interface
+- zope.pagetemplate
+- zope.tal
+- zope.tales
+- zope.i18nmessageid
+- psycopg2
+- M2Crypto
+- BeautifulSoup
+- docutils
+- distutils2
+
+Quick development setup
+-----------------------
+
+Make sure you read http://wiki.python.org/moin/CheeseShopDev#DevelopmentEnvironmentHints
+and you have a working PostgreSQL DB.
+
+Make sure your config.ini is up-to-date. Change CONFIG_FILE at the begining
+of pypi.wsgi, so it looks like this::
+
+    CONFIG_FILE = 'config.ini'
+
+Then, you can create a development environment like this, if you have
+virtualenv installed::
+
+    $ virtualenv --no-site-packages --distribute .
+    $ bin/easy_install cElementTree zope.interface zope.pagetemplate
+    $ bin/easy_install zope.tal zope.tales zope.i18nmessageid psycopg2
+    $ bin/easy_install  M2Crypto BeautifulSoup docutils
+
+Then you can launch the server using the pypi.wsgi script::
+
+    $ bin/python pypi.wsgi
+    Serving on port 8000...
+
+PyPI will be available in your browser at http://localhost:8000
 
-To run the PyPI software, you need:
-	cElementTree
-	zope.interface (in PyPI)
-	zope.pagetemplates (in PyPI)
-	zope.tal
-	zope.tales
-	zope.i18nmessageid
-	psycopg2 and PostgreSQL
-	M2Crypto

Modified: trunk/pypi/config.ini
==============================================================================
--- trunk/pypi/config.ini	(original)
+++ trunk/pypi/config.ini	Sat Apr 24 12:22:22 2010
@@ -7,6 +7,7 @@
 [webui]
 mailhost = mail.commonground.com.au
 adminemail = richard at commonground.com.au
+replyto = richard at commonground.com.au
 url = http://localhost/cgi-bin/pypi.cgi
 pydotorg = http://www.python.org/
 simple_script = /simple
@@ -18,9 +19,9 @@
 simple_sign_script = /serversig
 
 [logging]
-file = 
-mailhost = 
-fromaddr = 
+file =
+mailhost =
+fromaddr =
 toaddrs =
 
 [mirrors]

Modified: trunk/pypi/pkgbase_schema.sql
==============================================================================
--- trunk/pypi/pkgbase_schema.sql	(original)
+++ trunk/pypi/pkgbase_schema.sql	Sat Apr 24 12:22:22 2010
@@ -1,10 +1,10 @@
 -- Table structure for table: users
-CREATE TABLE users ( 
-   name TEXT PRIMARY KEY, 
-   password TEXT, 
-   email TEXT, 
+CREATE TABLE users (
+   name TEXT PRIMARY KEY,
+   password TEXT,
+   email TEXT,
    gpg_keyid TEXT,
-   last_login TIMESTAMP,
+   last_login TIMESTAMP
 );
 CREATE INDEX users_email_idx ON users(email);
 
@@ -12,7 +12,7 @@
 
 CREATE TABLE openids (
    id TEXT PRIMARY KEY,
-   name TEXT REFERENCES users,
+   name TEXT REFERENCES users
 );
 
 CREATE TABLE openid_sessions (
@@ -41,7 +41,7 @@
 CREATE TABLE cookies (
     cookie text PRIMARY KEY,
     name text references users,
-    last_seen timestamp,
+    last_seen timestamp
 );
 CREATE INDEX cookies_last_seen ON cookies(last_seen);
 
@@ -54,33 +54,33 @@
 CREATE INDEX rego_otk_otk_idx ON rego_otk(otk);
 
 -- Table structure for table: rego_otk
-CREATE TABLE rego_otk ( 
-   name TEXT REFERENCES users, 
+CREATE TABLE rego_otk (
+   name TEXT REFERENCES users,
    otk TEXT,
    date TIMESTAMP );
 CREATE INDEX rego_otk_name_idx ON rego_otk(name);
 
 
 -- Table structure for table: journals
-CREATE TABLE journals ( 
-   name TEXT, 
-   version TEXT, 
-   action TEXT, 
-   submitted_date TIMESTAMP, 
-   submitted_by TEXT REFERENCES users, 
+CREATE TABLE journals (
+   name TEXT,
+   version TEXT,
+   action TEXT,
+   submitted_date TIMESTAMP,
+   submitted_by TEXT REFERENCES users,
    submitted_from TEXT
 );
 CREATE INDEX journals_name_idx ON journals(name);
 CREATE INDEX journals_version_idx ON journals(version);
-CREATE INDEX journals_latest_releases ON 
-  journals(submitted_date, name, version) 
+CREATE INDEX journals_latest_releases ON
+  journals(submitted_date, name, version)
   WHERE version IS NOT NULL AND action='new release';
-CREATE INDEX journals_changelog ON 
+CREATE INDEX journals_changelog ON
   journals(submitted_date, name, version, action);
 
 -- Table structure for table: packages
-CREATE TABLE packages ( 
-   name TEXT PRIMARY KEY, 
+CREATE TABLE packages (
+   name TEXT PRIMARY KEY,
    stable_version TEXT,
    normalized_name TEXT,
    autohide BOOLEAN DEFAULT TRUE,
@@ -122,7 +122,7 @@
 
 
 -- Table structure for table: releases
-CREATE TABLE releases ( 
+CREATE TABLE releases (
    name TEXT REFERENCES packages ON UPDATE CASCADE,
    version TEXT,
    author TEXT,
@@ -137,25 +137,22 @@
    keywords TEXT,
    platform TEXT,
    download_url TEXT,
+   requires_python TEXT,
    cheesecake_installability_id INTEGER REFERENCES cheesecake_main_indices,
    cheesecake_documentation_id INTEGER REFERENCES cheesecake_main_indices,
    cheesecake_code_kwalitee_id INTEGER REFERENCES cheesecake_main_indices,
    _pypi_ordering INTEGER,
    _pypi_hidden BOOLEAN,
-   cheesecake_installability_id INTEGER REFERENCES cheesecake_main_indices,
-   cheesecake_documentation_id INTEGER REFERENCES cheesecake_main_indices,
-   cheesecake_code_kwalitee_id INTEGER REFERENCES cheesecake_main_indices,
    PRIMARY KEY (name, version)
 );
 CREATE INDEX release_pypi_hidden_idx ON releases(_pypi_hidden);
 
-
 -- Table structure for table: trove_classifiers
 -- l2, l3, l4, l5 is the corresponding parent;
 -- 0 if there is no parent on that level (each node is its
 -- own parent)
-CREATE TABLE trove_classifiers ( 
-   id INTEGER PRIMARY KEY, 
+CREATE TABLE trove_classifiers (
+   id INTEGER PRIMARY KEY,
    classifier TEXT UNIQUE,
    l2 INTEGER,
    l3 INTEGER,
@@ -219,12 +216,66 @@
 CREATE INDEX rel_obs_version_id_idx ON release_obsoletes(version);
 CREATE INDEX rel_obs_name_version_idx ON release_obsoletes (name,version);
 
+-- Table structure for table: release_requires_external
+CREATE TABLE release_requires_external (
+   name TEXT,
+   version TEXT,
+   specifier TEXT,
+   FOREIGN KEY (name, version) REFERENCES releases (name, version) ON UPDATE CASCADE
+);
+CREATE INDEX rel_req_ext_name_idx ON release_requires_external(name);
+CREATE INDEX rel_req_ext_version_id_idx ON release_requires_external(version);
+CREATE INDEX rel_req_ext_name_version_idx ON release_requires_external(name,version);
+
+-- Table structure for table: release_requires_dist
+CREATE TABLE release_requires_dist (
+   name TEXT,
+   version TEXT,
+   specifier TEXT,
+   FOREIGN KEY (name, version) REFERENCES releases (name, version) ON UPDATE CASCADE
+);
+CREATE INDEX rel_req_dist_name_idx ON release_requires_dist(name);
+CREATE INDEX rel_req_dist_version_id_idx ON release_requires_dist(version);
+CREATE INDEX rel_req_dist_name_version_idx ON release_requires_dist(name,version);
+
+-- Table structure for table: release_provides_dist
+CREATE TABLE release_provides_dist (
+   name TEXT,
+   version TEXT,
+   specifier TEXT,
+   FOREIGN KEY (name, version) REFERENCES releases (name, version) ON UPDATE CASCADE
+);
+CREATE INDEX rel_prov_dist_name_idx ON release_provides_dist(name);
+CREATE INDEX rel_prov_dist_version_id_idx ON release_provides_dist(version);
+CREATE INDEX rel_prov_dist_name_version_idx ON release_provides_dist(name,version);
+
+-- Table structure for table: release_obsoletes_dist
+CREATE TABLE release_obsoletes_dist (
+   name TEXT,
+   version TEXT,
+   specifier TEXT,
+   FOREIGN KEY (name, version) REFERENCES releases (name, version) ON UPDATE CASCADE
+);
+CREATE INDEX rel_obs_dist_name_idx ON release_obsoletes_dist(name);
+CREATE INDEX rel_obs_dist_version_id_idx ON release_obsoletes_dist(version);
+CREATE INDEX rel_obs_dist_name_version_idx ON release_obsoletes_dist(name,version);
+
+-- Table structure for table: release_project_url
+CREATE TABLE release_project_url (
+   name TEXT,
+   version TEXT,
+   specifier TEXT,
+   FOREIGN KEY (name, version) REFERENCES releases (name, version) ON UPDATE CASCADE
+);
+CREATE INDEX rel_proj_url_name_idx ON release_project_url(name);
+CREATE INDEX rel_proj_url_version_id_idx ON release_project_url(version);
+CREATE INDEX rel_proj_url_name_version_idx ON release_project_url(name,version);
 
 -- Table structure for table: package_files
 -- python version is only first two digits
 -- actual file path is constructed <py version>/<a-z>/<name>/<filename>
 -- we remember filename because it can differ
-CREATE TABLE release_files ( 
+CREATE TABLE release_files (
    name TEXT,
    version TEXT,
    python_version TEXT,
@@ -243,10 +294,10 @@
 
 
 -- Table structure for table: package_urls
-CREATE TABLE release_urls ( 
+CREATE TABLE release_urls (
    name TEXT,
    version TEXT,
-   url TEXT, 
+   url TEXT,
    packagetype TEXT,
    FOREIGN KEY (name, version) REFERENCES releases (name, version) ON UPDATE CASCADE
 );
@@ -266,9 +317,9 @@
 
 -- Table structure for table: roles
 -- Note: roles are Maintainer, Admin, Owner
-CREATE TABLE roles ( 
-   role_name TEXT, 
-   user_name TEXT REFERENCES users, 
+CREATE TABLE roles (
+   role_name TEXT,
+   user_name TEXT REFERENCES users,
    package_name TEXT REFERENCES packages ON UPDATE CASCADE
 );
 CREATE INDEX roles_pack_name_idx ON roles(package_name);
@@ -287,7 +338,7 @@
 -- Table structure for table: timestamps
 -- Note: stamp_name is ftp, http
 CREATE TABLE browse_tally (
-   trove_id INTEGER PRIMARY KEY,	
+   trove_id INTEGER PRIMARY KEY,
    tally INTEGER
 );
 
@@ -307,7 +358,7 @@
   in_reply_to INTEGER REFERENCES comments ON DELETE CASCADE
 );
 CREATE TABLE ratings(
-   id SERIAL UNIQUE;
+   id SERIAL UNIQUE,
    name TEXT,
    version TEXT,
    user_name TEXT REFERENCES users ON DELETE CASCADE,

Modified: trunk/pypi/pypi.wsgi
==============================================================================
--- trunk/pypi/pypi.wsgi	(original)
+++ trunk/pypi/pypi.wsgi	Sat Apr 24 12:22:22 2010
@@ -6,18 +6,24 @@
 
 store.keep_conn = True
 
+CONFIG_FILE = 'config.ini'
+
 class Request:
 
     def __init__(self, environ, start_response):
         self.start_response = start_response
-        self.rfile = cStringIO.StringIO(environ['wsgi.input'].read())
+        try:
+            length = int(environ['CONTENT_LENGTH'])
+        except ValueError:
+            length = 0
+        self.rfile = cStringIO.StringIO(environ['wsgi.input'].read(length))
         self.wfile = cStringIO.StringIO()
-        self.config = config.Config('/data/pypi/config.ini')
-    
+        self.config = config.Config(CONFIG_FILE )
+
     def send_response(self, code, message='no details available'):
         self.status = '%s %s' % (code, message)
         self.headers = []
-        
+
     def send_header(self, keyword, value):
         self.headers.append((keyword, value))
 
@@ -47,3 +53,11 @@
     r = Request(environ, start_response)
     webui.WebUI(r, environ).run()
     return [r.wfile.getvalue()]
+
+if __name__ == '__main__':
+    # very simple wsgi server so we can play locally
+    from wsgiref.simple_server import make_server
+    httpd = make_server('', 8000, application)
+    print "Serving on port 8000..."
+    httpd.serve_forever()
+

Modified: trunk/pypi/rpc.py
==============================================================================
--- trunk/pypi/rpc.py	(original)
+++ trunk/pypi/rpc.py	Sat Apr 24 12:22:22 2010
@@ -7,7 +7,7 @@
 
 class RequestHandler(SimpleXMLRPCDispatcher):
     """A request dispatcher for the PyPI XML-RPC API."""
-    
+
     def __init__(self):
         SimpleXMLRPCDispatcher.__init__(self, True, 'utf-8')
         self.register_function(list_packages)
@@ -23,7 +23,7 @@
         self.register_function(ratings)
         self.register_introspection_functions()
         self.register_multicall_functions()
-    
+
     def __call__(self, webui_obj):
         webui_obj.handler.send_response(200, 'OK')
         webui_obj.handler.send_header('Content-type', 'text/xml')
@@ -77,7 +77,9 @@
 def release_data(store, package_name, version):
     info = store.get_package(package_name, version).as_dict()
     del info['description_html']
-    for col in ('requires', 'provides', 'obsoletes'):
+    for col in ('requires', 'provides', 'obsoletes', 'requires_dist',
+                'obsoletes_dist', 'project_url', 'provides_dist',
+                'requires_external'):
         rows = store.get_release_relationships(package_name, version, col)
         info[col] = [row['specifier'] for row in rows]
     classifiers = [r[0] for r in store.get_release_classifiers(package_name,

Modified: trunk/pypi/store.py
==============================================================================
--- trunk/pypi/store.py	(original)
+++ trunk/pypi/store.py	Sat Apr 24 12:22:22 2010
@@ -79,6 +79,7 @@
     def utf8get(fields):
         if fields[n] is None: return fields[n]
         return fields[n].decode('utf-8', 'replace')
+
     return utf8get
 
 def itemgetter(n):
@@ -199,7 +200,6 @@
         journal entry.
         '''
         date = time.strftime('%Y-%m-%d %H:%M:%S', time.gmtime())
-
         cursor = self.get_cursor()
         # see if we're inserting or updating a package
         if not self.has_package(name):
@@ -268,7 +268,9 @@
                 old.append('classifiers')
 
             # get old classifiers list
-            for col in ('requires', 'provides', 'obsoletes'):
+            for col in ('requires', 'provides', 'obsoletes', 'requires_dist',
+                        'provides_dist', 'obsoletes_dist',
+                        'requires_external', 'project_url'):
                 relationships[col] = self.get_release_relationships(name,
                     version, col)
                 relationships[col].sort()
@@ -311,7 +313,10 @@
                 info['description_html'] = ''
 
             # perform the insert
-            cols = 'name version author author_email maintainer maintainer_email home_page license summary description description_html keywords platform download_url _pypi_ordering _pypi_hidden'.split()
+            cols = ('name version author author_email maintainer '
+                    'maintainer_email home_page license summary description '
+                    'description_html keywords platform requires_python '
+                    'download_url _pypi_ordering _pypi_hidden').split()
             args = tuple([info.get(k, None) for k in cols])
             params = ','.join(['%s']*len(cols))
             scols = ','.join(cols)
@@ -353,7 +358,9 @@
                     (name, version, trove_id))
 
         # handle relationship specifiers
-        for col in ('requires', 'provides', 'obsoletes'):
+        for col in ('requires', 'provides', 'obsoletes', 'requires_dist',
+                    'provides_dist', 'obsoletes_dist',
+                    'requires_external', 'project_url'):
             if not info.has_key(col) or relationships.get(col, []) == info[col]:
                 continue
             safe_execute(cursor, '''delete from release_%s where name=%%s
@@ -455,8 +462,8 @@
 
     _Package = FastResultRow('''name stable_version version author author_email
             maintainer maintainer_email home_page license summary description
-            description_html keywords platform download_url _pypi_ordering!
-            _pypi_hidden! cheesecake_installability_id!
+            description_html keywords platform requires_python download_url
+            _pypi_ordering! _pypi_hidden! cheesecake_installability_id!
             cheesecake_documentation_id! cheesecake_code_kwalitee_id!''')
     def get_package(self, name, version):
         ''' Retrieve info about the package from the database.
@@ -467,7 +474,8 @@
         sql = '''select packages.name as name, stable_version, version, author,
                   author_email, maintainer, maintainer_email, home_page,
                   license, summary, description, description_html, keywords,
-                  platform, download_url, _pypi_ordering, _pypi_hidden,
+                  platform, requires_python, download_url, _pypi_ordering,
+                  _pypi_hidden,
                   cheesecake_installability_id,
                   cheesecake_documentation_id,
                   cheesecake_code_kwalitee_id
@@ -656,6 +664,64 @@
         safe_execute(cursor, 'update packages set autohide=%s where name=%s',
                      [value, name])
 
+    def _get_package_url(self, name):
+        name = name.split()[0]
+        cursor = self.get_cursor()
+        sql = 'select * from packages where name=%s'
+        safe_execute(cursor, sql, (name, ))
+        exists = cursor.fetchone() is not None
+        if not exists:
+            return None
+        return self.config.url + '/' + name
+
+    def get_package_requires_dist(self, name, version):
+        cursor = self.get_cursor()
+        safe_execute(cursor, '''select specifier from release_requires_dist
+            where name=%s and version=%s ''', (name, version))
+        packages = []
+        for package in cursor.fetchall():
+            pack = {'name': package[0],
+                    'href': self._get_package_url(package[0])}
+            packages.append(pack)
+        return packages
+
+    def get_package_provides_dist(self, name, version):
+        cursor = self.get_cursor()
+        safe_execute(cursor, '''select specifier from release_provides_dist
+            where name=%s and version=%s ''', (name, version))
+        packages = []
+        for package in cursor.fetchall():
+            pack = {'name': package[0],
+                    'href': self._get_package_url(package[0])}
+            packages.append(pack)
+        return packages
+
+    def get_package_obsoletes_dist(self, name, version):
+        cursor = self.get_cursor()
+        safe_execute(cursor, '''select specifier from release_obsoletes_dist
+            where name=%s and version=%s ''', (name, version))
+        packages = []
+        for package in cursor.fetchall():
+            pack = {'name': package[0],
+                    'href': self._get_package_url(package[0])}
+            packages.append(pack)
+        return packages
+
+    def get_package_requires_external(self, name, version):
+        cursor = self.get_cursor()
+        safe_execute(cursor, '''select specifier from release_requires_external
+            where name=%s and version=%s ''', (name, version))
+        return [package[0] for package in cursor.fetchall()]
+
+    def get_package_project_url(self, name, version):
+        cursor = self.get_cursor()
+        safe_execute(cursor, '''select specifier from release_project_url
+            where name=%s and version=%s ''', (name, version))
+        project_urls = []
+        for project in cursor.fetchall():
+            project_urls.append(project[0].split(','))
+        return project_urls
+
     def get_package_comments(self, name):
         cursor = self.get_cursor()
         safe_execute(cursor, 'select comments from packages where name=%s',
@@ -877,7 +943,9 @@
 
         # delete ancillary table entries
         for tab in ('files', 'provides', 'requires', 'obsoletes',
-                'classifiers'):
+                'classifiers', 'requires_dist', 'provides_dist',
+                'obsoletes_dist', 'requires_external',
+                'project_url'):
             safe_execute(cursor, '''delete from release_%s where
                 name=%%s and version=%%s'''%tab, (name, version))
         safe_execute(cursor, 'delete from description_urls where name=%s and version=%s',
@@ -891,7 +959,7 @@
         safe_execute(cursor, '''insert into journals (name, version, action,
                 submitted_date, submitted_by, submitted_from) values
                 (%s, %s, %s, %s, %s, %s)''', (name, version, 'remove', date,
-                                              self.username, self.userip))        
+                                              self.username, self.userip))
 
     def remove_package(self, name):
         ''' Delete an entire package from the database.
@@ -904,7 +972,9 @@
 
         # delete ancillary table entries
         for tab in ('files', 'provides', 'requires', 'obsoletes',
-                'classifiers'):
+                'classifiers', 'requires_dist', 'provides_dist',
+                'obsoletes_dist', 'requires_external',
+                'project_url'):
             safe_execute(cursor, 'delete from release_%s where name=%%s'%tab,
                 (name, ))
 
@@ -969,7 +1039,7 @@
         '''Copy a user-s rating of package name from one version to another;
         return the comment if any'''
         cursor = self.get_cursor()
-        safe_execute(cursor, '''insert into ratings(name,version,user_name,date,rating) 
+        safe_execute(cursor, '''insert into ratings(name,version,user_name,date,rating)
                      select name,%s,user_name,now(),rating from ratings
                      where name=%s and version=%s and user_name=%s''',
                      (toversion, name, fromversion, self.username))
@@ -979,10 +1049,10 @@
         if cid:
             cid = cid[0]
             safe_execute(cursor, '''insert into comments(rating, user_name, date, message, in_reply_to)
-                     select currval('ratings_id_seq'), user_name, date, message, in_reply_to 
+                     select currval('ratings_id_seq'), user_name, date, message, in_reply_to
                      from comments where id=%s''', (cid,))
             safe_execute(cursor, '''insert into comments_journal(name, version, id, submitted_by, date, action)
-                     values(%s, %s, currval('comments_id_seq'), %s, now(), %s)''', (name, toversion, 
+                     values(%s, %s, currval('comments_id_seq'), %s, now(), %s)''', (name, toversion,
                      self.username, 'copied %s' % cid))
 
             safe_execute(cursor, '''select message from comments
@@ -996,7 +1066,7 @@
         safe_execute(cursor, """insert into comments_journal(name, version, id, submitted_by, date, action)
         select %s, %s, id, %s, now(), 'deleted' from ratings where user_name=%s and name=%s and version=%s""",
                      (name, version, self.username, self.username, name, version))
-        safe_execute(cursor, "delete from ratings where user_name=%s and name=%s and version=%s", 
+        safe_execute(cursor, "delete from ratings where user_name=%s and name=%s and version=%s",
                      (self.username, name, version))
 
     _Rating=FastResultRow('id! date! user rating!')
@@ -1004,7 +1074,7 @@
     def get_ratings(self, name, version):
         '''Return ratings,messages for a release.'''
         cursor = self.get_cursor()
-        safe_execute(cursor, '''select id, date, user_name, rating from ratings 
+        safe_execute(cursor, '''select id, date, user_name, rating from ratings
                      where name=%s and version=%s order by date''', (name, version))
         res = cursor.fetchall()
         safe_execute(cursor, '''select c.id, c.rating, c.user_name, c.date, c.message, c.in_reply_to from
@@ -1098,14 +1168,14 @@
     def has_rating(self, name, version):
         '''Check whether user has rated this release'''
         cursor = self.get_cursor()
-        safe_execute(cursor, '''select count(*) from ratings 
+        safe_execute(cursor, '''select count(*) from ratings
                      where user_name=%s and name=%s and version=%s''', (self.username, name, version))
         return cursor.fetchall()[0][0]
 
     def latest_rating(self, name):
         '''Return the user-s latest rating on any release, or None.'''
         cursor = self.get_cursor()
-        safe_execute(cursor, '''select version from ratings 
+        safe_execute(cursor, '''select version from ratings
                      where user_name=%s and name=%s order by date desc limit 1''', (self.username, name))
         res = cursor.fetchone()
         if res:
@@ -1321,7 +1391,7 @@
         '''
         cursor = self.get_cursor()
         safe_execute(cursor, 'insert into sshkeys(name, key) values(%s, %s)', (username, key))
-        
+
     def delete_sshkey(self, id):
         '''Delete an SSH key given by ID.
         '''

Modified: trunk/pypi/templates/display.pt
==============================================================================
--- trunk/pypi/templates/display.pt	(original)
+++ trunk/pypi/templates/display.pt	Sat Apr 24 12:22:22 2010
@@ -156,6 +156,64 @@
   </ul>
  </li>
 
+
+
+ <li tal:condition="data/requires_dist | nothing">
+   <strong>Requires Distributions</strong>
+   <ul class="nodot">
+     <li tal:repeat="require data/requires_dist">
+       <a tal:condition="require/href" tal:attributes="href require/href"
+           tal:content="require/name"/>
+        <tal:block tal:condition="not:require/href" 
+                   tal:replace="require/name">
+        </tal:block>
+     </li>
+   </ul>
+ </li>
+
+ <li tal:condition="data/provides_dist | nothing">
+   <strong>Provides Distributions</strong>
+   <ul class="nodot">
+     <li tal:repeat="provide data/provides_dist">
+       <a tal:condition="provide/href" tal:attributes="href provide/href"
+           tal:content="provide/name"/>
+       <tal:block tal:condition="not:provide/href" 
+                   tal:replace="provide/name">
+        </tal:block>
+
+     </li>
+   </ul>
+ </li>
+
+ <li tal:condition="data/obsoletes_dist | nothing">
+   <strong>Obsoletes Distributions</strong>
+   <ul class="nodot">
+     <li tal:repeat="obsolete data/obsoletes_dist">
+       <a tal:condition="obsolete/href" tal:attributes="href obsolete/href"
+           tal:content="obsolete/name"/>
+        <tal:block tal:condition="not:obsolete/href" 
+                   tal:replace="obsolete/name">
+        </tal:block>
+
+     </li>
+   </ul>
+ </li>
+
+ <li tal:condition="data/requires_external | nothing">
+   <strong>Requires Externals</strong>
+   <ul class="nodot">
+     <li tal:repeat="require data/requires_external">
+        <tal:block tal:replace="require">
+        </tal:block>
+     </li>
+   </ul>
+ </li>
+
+ <li tal:condition="data/requires_python | nothing">
+    <strong>Requires Python</strong>: <tal:block tal:replace="data/requires_python"></tal:block>
+ </li>
+
+
  <li tal:condition="data/roles/Owner | nothing">
   <strong>Package Index Owner:</strong>
   <span tal:content="python:', '.join(data['roles']['Owner'])" />

Modified: trunk/pypi/templates/standard_template.pt
==============================================================================
--- trunk/pypi/templates/standard_template.pt	(original)
+++ trunk/pypi/templates/standard_template.pt	Sat Apr 24 12:22:22 2010
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <metal:macro define-macro="page"
-             xmlns="http://www.w3.org/1999/xhtml" 
+             xmlns="http://www.w3.org/1999/xhtml"
              xmlns:tal="http://xml.zope.org/namespaces/tal"
              xmlns:metal="http://xml.zope.org/namespaces/metal">
   <html lang="en" xml:lang="en" xmlns="http://www.w3.org/1999/xhtml">
@@ -14,14 +14,111 @@
        <link rel="stylesheet" tal:attributes="href string:${app/config/pydotorg}css/pypi.css" type="text/css"/>
 
        <link rel="alternate" type="application/rss+xml" title="RSS: 30 latest updates" href="http://www.python.org/pypi?:action=rss"/>
-      <link media="screen" tal:attributes="href string:${app/config/pydotorg}styles/screen-switcher-default.css" type="text/css" id="screen-switcher-stylesheet" rel="stylesheet" />
-      <link media="screen" tal:attributes="href string:${app/config/pydotorg}styles/netscape4.css" type="text/css" rel="stylesheet" />
-      <link media="print" tal:attributes="href string:${app/config/pydotorg}styles/print.css" type="text/css" rel="stylesheet" />
-      <link media="screen" tal:attributes="href string:${app/config/pydotorg}styles/largestyles.css" type="text/css" rel="alternate stylesheet" title="large text" />
-      <link media="screen" tal:attributes="href string:${app/config/pydotorg}styles/defaultfonts.css" type="text/css" rel="alternate stylesheet" title="default fonts" />
-      <link rel="stylesheet" media="screen" href="/css/docutils.css" type="text/css"/>
-      
-      <metal:slot define-slot="head" />
+       <link media="screen" tal:attributes="href string:${app/config/pydotorg}styles/screen-switcher-default.css" type="text/css" id="screen-switcher-stylesheet" rel="stylesheet" />
+       <link media="screen" tal:attributes="href string:${app/config/pydotorg}styles/netscape4.css" type="text/css" rel="stylesheet" />
+       <link media="print" tal:attributes="href string:${app/config/pydotorg}styles/print.css" type="text/css" rel="stylesheet" />
+       <link media="screen" tal:attributes="href string:${app/config/pydotorg}styles/largestyles.css" type="text/css" rel="alternate stylesheet" title="large text" />
+       <link media="screen" tal:attributes="href string:${app/config/pydotorg}styles/defaultfonts.css" type="text/css" rel="alternate stylesheet" title="default fonts" />
+       <link rel="stylesheet" media="screen" href="/css/docutils.css" type="text/css"/>
+
+       <metal:slot define-slot="head" />
+
+       <style type="text/css" rel="stylesheet">
+	 #document-floating {
+  	   width: 15em;
+	 }
+	 .floating-box {
+	   float: right;
+	 }
+	 .homepage-box h4, #document-project-url h4, .externallinks h4
+	 {
+	 font-family: Verdana, Geneva, "Bitstream Vera Sans", Helvetica, sans-serif;
+	 font-size: 88%;
+	 font-weight: bold;
+	 padding: 3px 0 4px 12px;
+	 margin-bottom:8px;
+	 border-bottom:3px solid #d5ad42;
+	 background-color: #F4F4F4;
+	 text-align: left;
+	 z-index: 0;
+	 -moz-border-radius-topleft: 16px;
+	 }
+
+	 #document-project-url h4, .homepage-box h4, .externallinks h4 {
+	 -moz-border-radius-topleft:11px;
+	 background-color:#376A94;
+	 color:#FFFFFF;
+	 font-family:Arial,Verdana,Geneva,"Bitstream Vera Sans",Helvetica,sans-serif;
+	 font-size:94%;
+	 font-weight:bold;
+	 margin:0;
+	 padding:3px 0 4px 12px;
+	 text-align:left;
+	 z-index:0;
+	 }
+
+	 #document-project-url
+	 {
+	 font-family: Verdana, Geneva, "Bitstream Vera Sans", Helvetica, sans-serif;
+	 font-size:  98%;
+	 text-align: left;
+	 color: #3C4B6B;
+	 background-color: #F7F6F0;
+	 width: 15em;
+	 border: 1px solid #B7BECC;
+	 z-index: 999;
+	 float:right;
+	 margin: 0 12px 12px 12px;
+	 padding-bottom: 10px;
+	 list-style:none;
+	 -moz-border-radius-topleft: 12px;
+	 -moz-border-radius-bottomright: 12px;
+	 }
+
+	 #document-project-url ul
+	 {
+	 list-style: none;
+	 display: block;
+	 border:0;
+	 padding: 0;
+	 margin: 0 3em 0 1.1em;
+	 color: #3C4B6B;
+	 background: none;
+	 width: auto;
+	 font-size: 103%;
+	 font-family: Verdana, Geneva, "Bitstream Vera Sans", Helvetica, sans-serif;
+	 }
+
+	 #document-project-url a:link, #document-project-url a:visited
+	 {
+	 color:#3C4B6B;
+	 text-decoration:  none;
+	 }
+
+	 #document-project-url a:hover
+	 {
+	 color: #000000;
+	 text-decoration: underline;
+	 }
+
+	 #document-project-url h4 a:link, #document-project-url  h4 a:visited
+	 {
+	 color: #FFFFFF;
+	 }
+
+
+	 #document-project-url h4 a:hover
+	 {
+	 color: #FFFFFF;
+
+	 }
+	 #document-project-url form {
+	 margin:12px;
+	 }
+	 #document-project-url input {
+	 margin-top:3px;
+	 }
+       </style>
     </head>
     <body>
       <!--  Logo  -->
@@ -49,15 +146,15 @@
 <!-- XXX: reinstate this       <div id="screen-switcher"></div> -->
       </div>
       <div id="left-hand-navigation">
-        
+
         <!--  Main Menu NEED LEVEL TWO HEADER AND FOOTER -->
         <div id="menu">
-          <ul class="level-one">            
+          <ul class="level-one">
             <li class="selected">
               <a class="selected" tal:attributes="href app/url_path">Package Index</a>
-            
+
               <ul class="level-two">
-                
+
                 <tal:for-nav repeat="link app/navlinks_html">
                   <span tal:replace="structure link"/>
                 </tal:for-nav>
@@ -76,13 +173,13 @@
             </li>
 
 
-            
+
 
             <li class=""><a href="http://www.python.org/about" class="" title="About The Python Language">About</a>
             </li><li class=""><a href="http://www.python.org/news" class="" title="">News</a>
             </li><li class=""><a href="http://www.python.org/doc" class="" title="">Documentation</a>
             </li><li class=""><a href="http://www.python.org/download" title="">Download</a>
-            
+
 
             </li><li class=""><a href="http://www.python.org/community" class="" title="">Community</a>
             </li><li class=""><a href="http://www.python.org/psf" class="" title="Python Software Foundation">Foundation</a>
@@ -91,7 +188,7 @@
           </li>
           </ul>
         </div>
-              
+
       </div>
       <div id="content-body">
         <div id="body-main">
@@ -100,81 +197,96 @@
             <div id="breadcrumb">
               <a tal:attributes="href app/url_path">Package Index</a>
               <tal:block condition="exists:data/version">
-                <span class="breadcrumb-separator">&gt;</span> 
+                <span class="breadcrumb-separator">&gt;</span>
                 <a tal:content="data/title" tal:attributes="href python:app.packageURL(data['name'], data['version'])"></a>
               </tal:block>
-              
+
             </div>
 
-            <div id="document-navigation" style="overflow-y: auto; max-height: 15em; overflow-x: hidden;">
-              <tal:if-not-user condition="not: app/loggedin">
-                <h4>Not Logged In</h4>
-
-                <ul>
-		  <li tal:condition="app/username"><a tal:attributes="href python:app.link_action('clear_auth')">Clear Basic Auth</a></li>
-                  <li><a tal:attributes="href python: app.link_action('login')">Login</a></li>
-                  <li><a tal:attributes="href python: app.link_action('register_form')">Register</a></li>
-                  <li><a tal:attributes="href python: app.link_action('forgotten_password_form')">Lost Login?</a></li>
-		  <li>Use <a tal:attributes="href string:${app/url_path}?:action=openid">OpenID</a>
-                    <tal:block tal:repeat="prov data/providers">
-                     <a tal:attributes="href prov/login">
-                     <img tal:attributes="src prov/favicon; title prov/title"/>
-                     </a>
-                    </tal:block>
-                  </li>
-                </ul>
+	    <div id="document-floating" class="floating-box">
 
-              </tal:if-not-user>
+              <div id="document-navigation" class="floating-box">
+		<tal:if-not-user condition="not: app/loggedin">
+                  <h4>Not Logged In</h4>
+
+                  <ul>
+		    <li tal:condition="app/username"><a tal:attributes="href python:app.link_action('clear_auth')">Clear Basic Auth</a></li>
+                    <li><a tal:attributes="href python: app.link_action('login')">Login</a></li>
+                    <li><a tal:attributes="href python: app.link_action('register_form')">Register</a></li>
+                    <li><a tal:attributes="href python: app.link_action('forgotten_password_form')">Lost Login?</a></li>
+		    <li>Use <a tal:attributes="href string:${app/url_path}?:action=openid">OpenID</a>
+                      <tal:block tal:repeat="prov data/providers">
+			<a tal:attributes="href prov/login">
+                       <img tal:attributes="src prov/favicon; title prov/title"/>
+			</a>
+                      </tal:block>
+                    </li>
+                  </ul>
 
-              <tal:if-user condition="app/loggedin">
+		</tal:if-not-user>
 
-                <h4>Welcome <span tal:replace="app/username"/></h4>
-                <li>
-                  <a tal:attributes="href python:app.link_action('user_form')">Your details</a>
-                </li>
+		<tal:if-user condition="app/loggedin">
 
-                <tal:let define="packages python:app.store.user_packages(app.username)">
-                  <tal:if-packages condition="packages">
-                    <li>
-                      Your packages:
-                    </li>
-                    <ul>
-                      
+                  <h4>Welcome <span tal:replace="app/username"/></h4>
+                  <li>
+                    <a tal:attributes="href python:app.link_action('user_form')">Your details</a>
+                  </li>
+
+                  <tal:let define="packages python:app.store.user_packages(app.username)">
+                    <tal:if-packages condition="packages">
                       <li>
-                        <tal:block repeat="pkg_info packages">
-                          <a tal:attributes="href python: app.link_action('pkg_edit', name=pkg_info[0])"
-                             tal:content="python:pkg_info[0]">package</a><br />
-                        </tal:block>
+			Your packages:
                       </li>
-                    </ul>
-                  </tal:if-packages>
-                </tal:let>
-
-                <li>
-                  <a tal:attributes="href python:app.link_action('logout')">Logout</a>
-                </li>
-              </tal:if-user>
+                      <ul>
+			<li>
+                          <tal:block repeat="pkg_info packages">
+                            <a tal:attributes="href python: app.link_action('pkg_edit', name=pkg_info[0])"
+                               tal:content="python:pkg_info[0]">package</a><br />
+                          </tal:block>
+			</li>
+                      </ul>
+                    </tal:if-packages>
+                  </tal:let>
+
+                  <li>
+                    <a tal:attributes="href python:app.link_action('logout')">Logout</a>
+                  </li>
+		</tal:if-user>
 
-            
             </div>
 
+	    <tal:block condition="data/project_url | nothing">
+	      <div id="document-project-url" class="floating-box">
+		<h4>Project Links</h4>
+		<ul>
+		  <tal:block tal:repeat="project data/project_url">
+          <li>
+            <a tal:content="python:project[0]" tal:attributes="href python:project[1]"></a>
+		    </li>
+		  </tal:block>
+		</ul>
+	      </div>
+	    </tal:block>
+
+	    </div>
+
             <div class="section">
 
               <h1 tal:content="data/title"></h1>
 
               <metal:slot define-slot="body">
-                
+
                 This template has not replaced its body slot.
-                
+
               </metal:slot>
-              
+
             </div>
 
 
           </div>
           <div id="footer"><div id="credits">
             <a href="http://www.python.org/about/website">Website maintained by the Python community</a><br />
-            <a href="http://www.xs4all.com/" title="Web and email hosting provided by xs4all, Netherlands">hosting by xs4all</a> / 
+            <a href="http://www.xs4all.com/" title="Web and email hosting provided by xs4all, Netherlands">hosting by xs4all</a> /
             <a href="http://www.pollenation.net/" title="Design and content management system by Pollenation Internet, Yorkshire">design by pollenation</a>
           </div>
           Copyright © 1990-2010, <a href="http://www.python.org/psf">Python Software Foundation</a><br />

Modified: trunk/pypi/webui.py
==============================================================================
--- trunk/pypi/webui.py	(original)
+++ trunk/pypi/webui.py	Sat Apr 24 12:22:22 2010
@@ -4,6 +4,7 @@
 import re, zipfile, logging, pprint, sets, shutil, Cookie, subprocess
 from zope.pagetemplate.pagetemplatefile import PageTemplateFile
 from distutils.util import rfc822_escape
+from distutils2.metadata import DistributionMetadata
 
 # Importing M2Crypto patches urllib; don't let them do that
 orig = urllib.URLopener.open_https.im_func
@@ -335,7 +336,6 @@
         template_dir = os.path.join(os.path.dirname(__file__), 'templates')
         context['standard_template'] = PyPiPageTemplate(
             "standard_template.pt", template_dir)
-
         template = PyPiPageTemplate(filename, template_dir)
         content = template(**context)
 
@@ -1021,7 +1021,8 @@
     def display_pkginfo(self, name=None, version=None):
         '''Reconstruct and send a PKG-INFO metadata file.
         '''
-
+        # XXX tarek need to add 1.2 support here
+        #
         info, name, version = self._get_pkg_info(name, version)
         if not info:
             return self.fail('No such package / version',
@@ -1161,7 +1162,10 @@
 
 # RJ: disabled cheesecake because the (strange) errors were getting annoying
 #        columns = 'name version author author_email maintainer maintainer_email home_page download_url summary license description description_html keywords platform cheesecake_installability_id cheesecake_documentation_id cheesecake_code_kwalitee_id'.split()
-        columns = 'name version author author_email maintainer maintainer_email home_page download_url summary license description description_html keywords platform'.split()
+        columns = ('name version author author_email maintainer '
+                   'maintainer_email home_page requires_python download_url '
+                   'summary license description description_html keywords '
+                   'platform').split()
 
         release = {'description_html': ''}
         for column in columns:
@@ -1223,6 +1227,13 @@
             total += r['rating']
             tally[r['rating']] += 1
 
+        # New metadata
+        requires_dist = self.store.get_package_requires_dist(name, version)
+        provides_dist = self.store.get_package_provides_dist(name, version)
+        obsoletes_dist = self.store.get_package_obsoletes_dist(name, version)
+        project_url = self.store.get_package_project_url(name, version)
+        requires_external = self.store.get_package_requires_external(name, version)
+
         for c in comments:
             add = c, []
             parent_comments[c['id']] = add[1]
@@ -1259,7 +1270,7 @@
                         rating = ', %s points' % rating
                 else:
                     rating = ''
-                result.append("<li>%s (%s%s):<br/>%s %s" % 
+                result.append("<li>%s (%s%s):<br/>%s %s" %
                                 (c['user'], date, rating, message, reply))
                 if children:
                     result.extend(render_comments(children, False))
@@ -1288,7 +1299,13 @@
                             usinglatest=using_latest,
                             latestversion=latest_version,
                             latestversionurl=latest_version_url,
-                            action=self.link_action())
+                            action=self.link_action(),
+                            requires_dist=requires_dist,
+                            provides_dist=provides_dist,
+                            obsoletes_dist=obsoletes_dist,
+                            requires_external=requires_external,
+                            project_url=project_url,
+                            requires_python=release.get('requires_python', ''))
 
     def index(self, nav_current='index', releases=None):
         ''' Print up an index page
@@ -1569,7 +1586,10 @@
             data['platform'] = ','.join(data['platform'])
 
         # make sure relationships are lists
-        for name in ('requires', 'provides', 'obsoletes'):
+        for name in ('requires', 'provides', 'obsoletes',
+                     'requires_dist', 'provides_dist',
+                     'obsoletes_dist',
+                     'requires_external', 'project_url'):
             if data.has_key(name) and not isinstance(data[name],
                     types.ListType):
                 data[name] = [data[name]]
@@ -1671,7 +1691,10 @@
             v = self.form[k]
             if k == '_pypi_hidden':
                 v = v == '1'
-            elif k in ('requires', 'provides', 'obsoletes'):
+            elif k in ('requires', 'provides', 'obsoletes',
+                       'requires_dist', 'provides_dist',
+                       'obsoletes_dist',
+                       'requires_external', 'project_url'):
                 if not isinstance(v, list):
                     v = [x.strip() for x in re.split('\s*[\r\n]\s*', v)]
                 else:
@@ -1689,7 +1712,10 @@
             data[k.lower()] = v
 
         # make sure relationships are lists
-        for name in ('requires', 'provides', 'obsoletes'):
+        for name in ('requires', 'provides', 'obsoletes',
+                     'requires_dist', 'provides_dist',
+                     'obsoletes_dist',
+                     'requires_external', 'project_url'):
             if data.has_key(name) and not isinstance(data[name],
                     types.ListType):
                 data[name] = [data[name]]
@@ -1716,6 +1742,23 @@
         self.write_template('message.pt', title='Package verification',
             message='Validated OK')
 
+    def _validate_metadata_1_2(self, data):
+        # loading the metadata into
+        # a DistributionMetadata instance
+        # so we can use its check() method
+        metadata = DistributionMetadata()
+        for key, value in data.items():
+            metadata[key] = value
+        metadata['Metadata-Version'] = '1.2'
+        missing, warnings = metadata.check()
+
+        # raising the first problem
+        if len(missing) > 0:
+            raise ValueError, '"%s" is missing' % missing[0]
+
+        if len(warnings) > 0:
+            raise ValueError, warnings[0]
+
     def validate_metadata(self, data):
         ''' Validate the contents of the metadata.
         '''
@@ -1724,7 +1767,10 @@
         if not data.get('version', ''):
             raise ValueError, 'Missing required field "version"'
         if data.has_key('metadata_version'):
+            metadata_version = data['metadata_version']
             del data['metadata_version']
+        else:
+            metadata_version = '1.0'  # default
 
         # Traditionally, package names are restricted only for
         # technical reasons; / is not allowed because it may be
@@ -1750,6 +1796,10 @@
             except ValueError, message:
                 raise ValueError, 'Bad "provides" syntax: %s'%message
 
+        # check PEP 345 fields
+        if metadata_version == '1.2':
+            self._validate_metadata_1_2(data)
+
         # check classifiers
         if data.has_key('classifiers'):
             d = {}
@@ -1866,7 +1916,7 @@
         if not self.form.has_key('msg'):
             raise FormError
         comment = self.store.get_comment(self.form['msg'])
-        self.write_template('comment.pt', title='Reply to comment', 
+        self.write_template('comment.pt', title='Reply to comment',
                             comment=comment)
 
     def addcomment(self):
@@ -1890,7 +1940,7 @@
         comment_email(self.store, name, version, self.username, comment, [orig['user']])
 
         return self.display(name=name, version=version)
-        
+
 
     def delcomment(self):
         if not self.authenticated:
@@ -2716,15 +2766,15 @@
         return self.register_form()
 
     def rp_discovery(self):
-        payload = '''<xrds:XRDS  
-                xmlns:xrds="xri://$xrds"  
-                xmlns="xri://$xrd*($v*2.0)">  
-                <XRD>  
-                     <Service priority="1">  
-                              <Type>http://specs.openid.net/auth/2.0/return_to</Type>  
-                              <URI>%s</URI>  
-                     </Service>  
-                </XRD>  
+        payload = '''<xrds:XRDS
+                xmlns:xrds="xri://$xrds"
+                xmlns="xri://$xrd*($v*2.0)">
+                <XRD>
+                     <Service priority="1">
+                              <Type>http://specs.openid.net/auth/2.0/return_to</Type>
+                              <URI>%s</URI>
+                     </Service>
+                </XRD>
                 </xrds:XRDS>
         ''' % (self.config.url+'?:action=openid_return')
         self.handler.send_response(200)


More information about the Pypi-checkins mailing list