Hello community,

here is the log from the commit of package python-web.py for openSUSE:Factory 
checked in at 2020-11-10 13:46:27
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-web.py (Old)
 and      /work/SRC/openSUSE:Factory/.python-web.py.new.11331 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-web.py"

Tue Nov 10 13:46:27 2020 rev:13 rq:847353 version:0.62

Changes:
--------
--- /work/SRC/openSUSE:Factory/python-web.py/python-web.py.changes      
2020-08-13 10:12:50.102591522 +0200
+++ /work/SRC/openSUSE:Factory/.python-web.py.new.11331/python-web.py.changes   
2020-11-10 13:53:27.962848553 +0100
@@ -1,0 +2,9 @@
+Mon Nov  9 21:01:36 UTC 2020 - Michael Ströder <[email protected]>
+
+- version update to 0.62:
+  * Fixed: application.load() assumes ctx.path will be a latin1 string #687
+  * Fixed: can not reset session data to same value as initialized. #683
+  * Fixed: can not set session expire time. #655
+  * Fixed: not export session store `MemoryStore`.
+
+-------------------------------------------------------------------

Old:
----
  web.py-0.61.tar.gz

New:
----
  web.py-0.62.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ python-web.py.spec ++++++
--- /var/tmp/diff_new_pack.ElIAe1/_old  2020-11-10 13:53:28.878846822 +0100
+++ /var/tmp/diff_new_pack.ElIAe1/_new  2020-11-10 13:53:28.882846814 +0100
@@ -15,11 +15,12 @@
 # Please submit bugfixes or comments via https://bugs.opensuse.org/
 #
 
+
 %define skip_python2 1
 
 %{?!python_module:%define python_module() python-%{**} python3-%{**}}
 Name:           python-web.py
-Version:        0.61
+Version:        0.62
 Release:        0
 Summary:        web.py: makes web apps
 License:        SUSE-Public-Domain AND BSD-3-Clause

++++++ web.py-0.61.tar.gz -> web.py-0.62.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/web.py-0.61/PKG-INFO new/web.py-0.62/PKG-INFO
--- old/web.py-0.61/PKG-INFO    2020-07-27 17:34:14.777639600 +0200
+++ new/web.py-0.62/PKG-INFO    2020-11-09 12:20:52.308910800 +0100
@@ -1,6 +1,6 @@
 Metadata-Version: 2.1
 Name: web.py
-Version: 0.61
+Version: 0.62
 Summary: web.py: makes web apps
 Home-page: http://webpy.org/
 Author: Aaron Swartz
@@ -15,11 +15,11 @@
         [![build 
status](https://secure.travis-ci.org/webpy/webpy.png?branch=master)](https://travis-ci.org/webpy/webpy)
         [![Codecov Test 
Coverage](https://codecov.io/gh/webpy/webpy/branch/master/graphs/badge.svg?style=flat)](https://codecov.io/gh/webpy/webpy)
         
-        The latest stable release `0.61` only supports Python >= 3.5.
+        The latest stable release `0.62` only supports Python >= 3.5.
         To install it, please run:
         ```
         # For Python 3
-        python3 -m pip install web.py==0.61
+        python3 -m pip install web.py==0.62
         ```
         
         If you are still using Python 2.7, then please use web.py version 0.51
@@ -32,12 +32,13 @@
         You can also download it from [GitHub 
Releases](https://github.com/webpy/webpy/releases)
         page, then install it manually:
         ```
-        unzip webpy-0.61.zip
-        cd webpy-0.61/
+        unzip webpy-0.62.zip
+        cd webpy-0.62/
         python3 setup.py install
         ```
         
         Note: `0.5x` (e.g. 0.50, 0.51) are our last releases which support 
Python 2.
+        Note: `0.6x` (e.g. 0.60, 0.61, 0.62) are our last releases which 
support Python 3.5.
         
 Platform: any
 Classifier: License :: Public Domain
@@ -47,5 +48,6 @@
 Classifier: Programming Language :: Python :: 3.6
 Classifier: Programming Language :: Python :: 3.7
 Classifier: Programming Language :: Python :: 3.8
+Classifier: Programming Language :: Python :: 3.9
 Requires-Python: >=3.5
 Description-Content-Type: text/markdown
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/web.py-0.61/README.md new/web.py-0.62/README.md
--- old/web.py-0.61/README.md   2020-07-27 16:43:53.000000000 +0200
+++ new/web.py-0.62/README.md   2020-11-09 10:11:31.000000000 +0100
@@ -5,11 +5,11 @@
 [![build 
status](https://secure.travis-ci.org/webpy/webpy.png?branch=master)](https://travis-ci.org/webpy/webpy)
 [![Codecov Test 
Coverage](https://codecov.io/gh/webpy/webpy/branch/master/graphs/badge.svg?style=flat)](https://codecov.io/gh/webpy/webpy)
 
-The latest stable release `0.61` only supports Python >= 3.5.
+The latest stable release `0.62` only supports Python >= 3.5.
 To install it, please run:
 ```
 # For Python 3
-python3 -m pip install web.py==0.61
+python3 -m pip install web.py==0.62
 ```
 
 If you are still using Python 2.7, then please use web.py version 0.51
@@ -22,9 +22,10 @@
 You can also download it from [GitHub 
Releases](https://github.com/webpy/webpy/releases)
 page, then install it manually:
 ```
-unzip webpy-0.61.zip
-cd webpy-0.61/
+unzip webpy-0.62.zip
+cd webpy-0.62/
 python3 setup.py install
 ```
 
 Note: `0.5x` (e.g. 0.50, 0.51) are our last releases which support Python 2.
+Note: `0.6x` (e.g. 0.60, 0.61, 0.62) are our last releases which support 
Python 3.5.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/web.py-0.61/setup.py new/web.py-0.62/setup.py
--- old/web.py-0.61/setup.py    2020-07-27 16:43:53.000000000 +0200
+++ new/web.py-0.62/setup.py    2020-11-09 10:11:31.000000000 +0100
@@ -36,5 +36,6 @@
         "Programming Language :: Python :: 3.6",
         "Programming Language :: Python :: 3.7",
         "Programming Language :: Python :: 3.8",
+        "Programming Language :: Python :: 3.9",
     ],
 )
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/web.py-0.61/web/__init__.py 
new/web.py-0.62/web/__init__.py
--- old/web.py-0.61/web/__init__.py     2020-07-27 16:43:53.000000000 +0200
+++ new/web.py-0.62/web/__init__.py     2020-11-09 10:11:31.000000000 +0100
@@ -24,7 +24,7 @@
 from .webapi import *  # noqa: F401,F403
 from .wsgi import *  # noqa: F401,F403
 
-__version__ = "0.61"
+__version__ = "0.62"
 __author__ = [
     "Aaron Swartz <[email protected]>",
     "Anand Chitipothu <[email protected]>",
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/web.py-0.61/web/application.py 
new/web.py-0.62/web/application.py
--- old/web.py-0.61/web/application.py  2020-07-27 16:43:53.000000000 +0200
+++ new/web.py-0.62/web/application.py  2020-11-09 09:31:55.000000000 +0100
@@ -360,8 +360,7 @@
         return wsgi.runwsgi(self.wsgifunc(*middleware))
 
     def stop(self):
-        """Stops the http server started by run.
-        """
+        """Stops the http server started by run."""
         if httpserver.server:
             httpserver.server.stop()
             httpserver.server = None
@@ -454,13 +453,17 @@
         ctx.realhome = ctx.home
         ctx.ip = env.get("REMOTE_ADDR")
         ctx.method = env.get("REQUEST_METHOD")
-        ctx.path = env.get("PATH_INFO").encode("latin1").decode("utf8")
+        try:
+            ctx.path = bytes(env.get("PATH_INFO"), "latin1").decode("utf8")
+        except UnicodeDecodeError:  # If there are Unicode characters...
+            ctx.path = env.get("PATH_INFO")
 
         # http://trac.lighttpd.net/trac/ticket/406 requires:
-        if env.get("SERVER_SOFTWARE", "").startswith("lighttpd/"):
+        if env.get("SERVER_SOFTWARE", "").startswith(("lighttpd/", "nginx/")):
             ctx.path = lstrips(env.get("REQUEST_URI").split("?")[0], 
ctx.homepath)
-            # Apache and CherryPy webservers unquote the url but lighttpd 
doesn't.
-            # unquote explicitly for lighttpd to make ctx.path uniform across 
all servers.
+            # Apache and CherryPy webservers unquote urls but lighttpd and 
nginx do not.
+            # Unquote explicitly for lighttpd and nginx to make ctx.path 
uniform across
+            # all servers.
             ctx.path = unquote(ctx.path)
 
         if env.get("QUERY_STRING"):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/web.py-0.61/web/browser.py 
new/web.py-0.62/web/browser.py
--- old/web.py-0.61/web/browser.py      2020-07-27 16:43:53.000000000 +0200
+++ new/web.py-0.62/web/browser.py      2020-11-08 19:37:39.000000000 +0100
@@ -239,17 +239,17 @@
 class AppBrowser(Browser):
     """Browser interface to test web.py apps.
 
-        b = AppBrowser(app)
-        b.open('/')
-        b.follow_link(text='Login')
+    b = AppBrowser(app)
+    b.open('/')
+    b.follow_link(text='Login')
 
-        b.select_form(name='login')
-        b['username'] = 'joe'
-        b['password'] = 'secret'
-        b.submit()
+    b.select_form(name='login')
+    b['username'] = 'joe'
+    b['password'] = 'secret'
+    b.submit()
 
-        assert b.path == '/'
-        assert 'Welcome joe' in b.get_text()
+    assert b.path == '/'
+    assert 'Welcome joe' in b.get_text()
     """
 
     def __init__(self, app):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/web.py-0.61/web/db.py new/web.py-0.62/web/db.py
--- old/web.py-0.61/web/db.py   2020-07-27 16:43:53.000000000 +0200
+++ new/web.py-0.62/web/db.py   2020-11-08 19:37:39.000000000 +0100
@@ -154,15 +154,15 @@
     def __init__(self, items=None):
         r"""Creates a new SQLQuery.
 
-            >>> SQLQuery("x")
-            <sql: 'x'>
-            >>> q = SQLQuery(['SELECT * FROM ', 'test', ' WHERE x=', 
SQLParam(1)])
-            >>> q
-            <sql: 'SELECT * FROM test WHERE x=1'>
-            >>> q.query(), q.values()
-            ('SELECT * FROM test WHERE x=%s', [1])
-            >>> SQLQuery(SQLParam(1))
-            <sql: '1'>
+        >>> SQLQuery("x")
+        <sql: 'x'>
+        >>> q = SQLQuery(['SELECT * FROM ', 'test', ' WHERE x=', SQLParam(1)])
+        >>> q
+        <sql: 'SELECT * FROM test WHERE x=1'>
+        >>> q.query(), q.values()
+        ('SELECT * FROM test WHERE x=%s', [1])
+        >>> SQLQuery(SQLParam(1))
+        <sql: '1'>
         """
         if items is None:
             self.items = []
@@ -327,12 +327,12 @@
 
 def _sqllist(values):
     """
-        >>> _sqllist([1, 2, 3])
-        <sql: '(1, 2, 3)'>
-        >>> _sqllist(set([5, 1, 3, 2]))
-        <sql: '(1, 2, 3, 5)'>
-        >>> _sqllist((5, 1, 3, 2, 2, 5))
-        <sql: '(1, 2, 3, 5)'>
+    >>> _sqllist([1, 2, 3])
+    <sql: '(1, 2, 3)'>
+    >>> _sqllist(set([5, 1, 3, 2]))
+    <sql: '(1, 2, 3, 5)'>
+    >>> _sqllist((5, 1, 3, 2, 2, 5))
+    <sql: '(1, 2, 3, 5)'>
     """
     items = []
     items.append("(")
@@ -486,8 +486,7 @@
 
 
 class BaseResultSet:
-    """Base implementation of Result Set, the result of a db query.
-    """
+    """Base implementation of Result Set, the result of a db query."""
 
     def __init__(self, cursor):
         self.cursor = cursor
@@ -542,8 +541,7 @@
 
 
 class ResultSet(BaseResultSet):
-    """The result of a database query.
-    """
+    """The result of a database query."""
 
     def __len__(self):
         return int(self.cursor.rowcount)
@@ -656,8 +654,7 @@
     """Database"""
 
     def __init__(self, db_module, keywords):
-        """Creates a database.
-        """
+        """Creates a database."""
         # some DB implementations take optional parameter `driver` to use a
         # specific driver module but it should not be passed to `connect`.
         keywords.pop("driver", None)
@@ -783,8 +780,7 @@
         return out
 
     def _process_query(self, sql_query):
-        """Takes the SQLQuery object and returns query string and parameters.
-        """
+        """Takes the SQLQuery object and returns query string and 
parameters."""
         paramstyle = getattr(self, "paramstyle", "pyformat")
         query = sql_query.query(paramstyle)
         params = sql_query.values()
@@ -1283,8 +1279,7 @@
 
 
 def import_driver(drivers, preferred=None):
-    """Import the first available driver or preferred driver.
-    """
+    """Import the first available driver or preferred driver."""
     if preferred:
         drivers = (preferred,)
 
@@ -1326,8 +1321,7 @@
 
 
 class FirebirdDB(DB):
-    """Firebird Database.
-    """
+    """Firebird Database."""
 
     def __init__(self, **keywords):
         try:
@@ -1372,8 +1366,7 @@
         DB.__init__(self, db, keywords)
 
     def _process_query(self, sql_query):
-        """Takes the SQLQuery object and returns query string and parameters.
-        """
+        """Takes the SQLQuery object and returns query string and 
parameters."""
         # MSSQLDB expects params to be a tuple.
         # Overwriting the default implementation to convert params to tuple.
         paramstyle = getattr(self, "paramstyle", "pyformat")
@@ -1395,14 +1388,14 @@
     def _test(self):
         """Test LIMIT.
 
-            Fake presence of pymssql module for running tests.
-            >>> import sys
-            >>> sys.modules['pymssql'] = sys.modules['sys']
-
-            MSSQL has TOP clause instead of LIMIT clause.
-            >>> db = MSSQLDB(db='test', user='joe', pw='secret')
-            >>> db.select('foo', limit=4, _test=True)
-            <sql: 'SELECT * TOP 4 FROM foo'>
+        Fake presence of pymssql module for running tests.
+        >>> import sys
+        >>> sys.modules['pymssql'] = sys.modules['sys']
+
+        MSSQL has TOP clause instead of LIMIT clause.
+        >>> db = MSSQLDB(db='test', user='joe', pw='secret')
+        >>> db.select('foo', limit=4, _test=True)
+        <sql: 'SELECT * TOP 4 FROM foo'>
         """
         pass
 
@@ -1615,8 +1608,7 @@
         self.text = ""
 
     def parse(self, text):
-        """Parses the given text and returns a parse tree.
-        """
+        """Parses the given text and returns a parse tree."""
         self.reset()
         self.text = text
         return self.parse_all()
@@ -1697,8 +1689,7 @@
 
 
 class SafeEval(object):
-    """Safe evaluator for binding params to db queries.
-    """
+    """Safe evaluator for binding params to db queries."""
 
     def safeeval(self, text, mapping):
         nodes = Parser().parse(text)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/web.py-0.61/web/form.py new/web.py-0.62/web/form.py
--- old/web.py-0.61/web/form.py 2020-07-27 16:43:53.000000000 +0200
+++ new/web.py-0.62/web/form.py 2020-11-08 19:37:39.000000000 +0100
@@ -270,10 +270,10 @@
 class Textbox(Input):
     """Textbox input.
 
-        >>> Textbox(name='foo', value='bar').render()
-        u'<input id="foo" name="foo" type="text" value="bar"/>'
-        >>> Textbox(name='foo', value=0).render()
-        u'<input id="foo" name="foo" type="text" value="0"/>'
+    >>> Textbox(name='foo', value='bar').render()
+    u'<input id="foo" name="foo" type="text" value="bar"/>'
+    >>> Textbox(name='foo', value=0).render()
+    u'<input id="foo" name="foo" type="text" value="0"/>'
     """
 
     def get_type(self):
@@ -283,8 +283,8 @@
 class Password(Input):
     """Password input.
 
-        >>> Password(name='password', value='secret').render()
-        u'<input id="password" name="password" type="password" 
value="secret"/>'
+    >>> Password(name='password', value='secret').render()
+    u'<input id="password" name="password" type="password" value="secret"/>'
     """
 
     def get_type(self):
@@ -294,8 +294,8 @@
 class Textarea(Input):
     """Textarea input.
 
-        >>> Textarea(name='foo', value='bar').render()
-        u'<textarea id="foo" name="foo">bar</textarea>'
+    >>> Textarea(name='foo', value='bar').render()
+    u'<textarea id="foo" name="foo">bar</textarea>'
     """
 
     def render(self):
@@ -308,10 +308,10 @@
 class Dropdown(Input):
     r"""Dropdown/select input.
 
-        >>> Dropdown(name='foo', args=['a', 'b', 'c'], value='b').render()
-        u'<select id="foo" name="foo">\n  <option value="a">a</option>\n  
<option selected="selected" value="b">b</option>\n  <option 
value="c">c</option>\n</select>\n'
-        >>> Dropdown(name='foo', args=[('a', 'aa'), ('b', 'bb'), ('c', 'cc')], 
value='b').render()
-        u'<select id="foo" name="foo">\n  <option value="a">aa</option>\n  
<option selected="selected" value="b">bb</option>\n  <option 
value="c">cc</option>\n</select>\n'
+    >>> Dropdown(name='foo', args=['a', 'b', 'c'], value='b').render()
+    u'<select id="foo" name="foo">\n  <option value="a">a</option>\n  <option 
selected="selected" value="b">b</option>\n  <option 
value="c">c</option>\n</select>\n'
+    >>> Dropdown(name='foo', args=[('a', 'aa'), ('b', 'bb'), ('c', 'cc')], 
value='b').render()
+    u'<select id="foo" name="foo">\n  <option value="a">aa</option>\n  <option 
selected="selected" value="b">bb</option>\n  <option 
value="c">cc</option>\n</select>\n'
     """
 
     def __init__(self, name, args, *validators, **attrs):
@@ -356,10 +356,10 @@
 class GroupedDropdown(Dropdown):
     r"""Grouped Dropdown/select input.
 
-        >>> GroupedDropdown(name='car_type', args=(('Swedish Cars', ('Volvo', 
'Saab')), ('German Cars', ('Mercedes', 'Audi'))), value='Audi').render()
-        u'<select id="car_type" name="car_type">\n  <optgroup label="Swedish 
Cars">\n    <option value="Volvo">Volvo</option>\n    <option 
value="Saab">Saab</option>\n  </optgroup>\n  <optgroup label="German Cars">\n   
 <option value="Mercedes">Mercedes</option>\n    <option selected="selected" 
value="Audi">Audi</option>\n  </optgroup>\n</select>\n'
-        >>> GroupedDropdown(name='car_type', args=(('Swedish Cars', (('v', 
'Volvo'), ('s', 'Saab'))), ('German Cars', (('m', 'Mercedes'), ('a', 
'Audi')))), value='a').render()
-        u'<select id="car_type" name="car_type">\n  <optgroup label="Swedish 
Cars">\n    <option value="v">Volvo</option>\n    <option 
value="s">Saab</option>\n  </optgroup>\n  <optgroup label="German Cars">\n    
<option value="m">Mercedes</option>\n    <option selected="selected" 
value="a">Audi</option>\n  </optgroup>\n</select>\n'
+    >>> GroupedDropdown(name='car_type', args=(('Swedish Cars', ('Volvo', 
'Saab')), ('German Cars', ('Mercedes', 'Audi'))), value='Audi').render()
+    u'<select id="car_type" name="car_type">\n  <optgroup label="Swedish 
Cars">\n    <option value="Volvo">Volvo</option>\n    <option 
value="Saab">Saab</option>\n  </optgroup>\n  <optgroup label="German Cars">\n   
 <option value="Mercedes">Mercedes</option>\n    <option selected="selected" 
value="Audi">Audi</option>\n  </optgroup>\n</select>\n'
+    >>> GroupedDropdown(name='car_type', args=(('Swedish Cars', (('v', 
'Volvo'), ('s', 'Saab'))), ('German Cars', (('m', 'Mercedes'), ('a', 
'Audi')))), value='a').render()
+    u'<select id="car_type" name="car_type">\n  <optgroup label="Swedish 
Cars">\n    <option value="v">Volvo</option>\n    <option 
value="s">Saab</option>\n  </optgroup>\n  <optgroup label="German Cars">\n    
<option value="m">Mercedes</option>\n    <option selected="selected" 
value="a">Audi</option>\n  </optgroup>\n</select>\n'
 
     """
 
@@ -471,8 +471,8 @@
 class Hidden(Input):
     """Hidden Input.
 
-        >>> Hidden(name='foo', value='bar').render()
-        u'<input id="foo" name="foo" type="hidden" value="bar"/>'
+    >>> Hidden(name='foo', value='bar').render()
+    u'<input id="foo" name="foo" type="hidden" value="bar"/>'
     """
 
     def is_hidden(self):
@@ -485,8 +485,8 @@
 class File(Input):
     """File input.
 
-        >>> File(name='f', accept=".doc,.docx,.xml").render()
-        u'<input accept=".doc,.docx,.xml" id="f" name="f" type="file"/>'
+    >>> File(name='f', accept=".doc,.docx,.xml").render()
+    u'<input accept=".doc,.docx,.xml" id="f" name="f" type="file"/>'
     """
 
     def get_type(self):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/web.py-0.61/web/httpserver.py 
new/web.py-0.62/web/httpserver.py
--- old/web.py-0.61/web/httpserver.py   2020-07-27 16:43:53.000000000 +0200
+++ new/web.py-0.62/web/httpserver.py   2020-11-08 19:37:39.000000000 +0100
@@ -28,13 +28,13 @@
 
 def runbasic(func, server_address=("0.0.0.0", 8080)):
     """
-    Runs a simple HTTP server hosting WSGI app `func`. The directory `static/`
-    is hosted statically.
+      Runs a simple HTTP server hosting WSGI app `func`. The directory 
`static/`
+      is hosted statically.
 
-    Based on [WsgiServer][ws] from [Colin Stewart][cs].
+      Based on [WsgiServer][ws] from [Colin Stewart][cs].
 
-  [ws]: 
http://www.owlfish.com/software/wsgiutils/documentation/wsgi-server-api.html
-  [cs]: http://www.owlfish.com/
+    [ws]: 
http://www.owlfish.com/software/wsgiutils/documentation/wsgi-server-api.html
+    [cs]: http://www.owlfish.com/
     """
     # Copyright (c) 2004 Colin Stewart (http://www.owlfish.com/)
     # Modified somewhat for simplicity
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/web.py-0.61/web/session.py 
new/web.py-0.62/web/session.py
--- old/web.py-0.61/web/session.py      2020-07-27 16:43:53.000000000 +0200
+++ new/web.py-0.62/web/session.py      2020-11-08 19:37:39.000000000 +0100
@@ -25,7 +25,7 @@
 from base64 import encodebytes, decodebytes
 
 
-__all__ = ["Session", "SessionExpired", "Store", "DiskStore", "DBStore"]
+__all__ = ["Session", "SessionExpired", "Store", "DiskStore", "DBStore", 
"MemoryStore"]
 
 web.config.session_parameters = utils.storage(
     {
@@ -50,8 +50,7 @@
 
 
 class Session(object):
-    """Session management for web.py
-    """
+    """Session management for web.py"""
 
     __slots__ = [
         "store",
@@ -149,16 +148,12 @@
         del current_values["session_id"]
         del current_values["ip"]
 
-        if not self.get("_killed") and current_values != self._initializer:
+        if not self.get("_killed"):
             self._setcookie(self.session_id)
             self.store[self.session_id] = dict(self._data)
         else:
             if web.cookies().get(self._config.cookie_name):
-                self._setcookie(
-                    self.session_id,
-                    expires=self._config.timeout,
-                    samesite=self._config.get("samesite"),
-                )
+                self._setcookie(self.session_id, expires=-1)
 
     def _setcookie(self, session_id, expires="", **kw):
         cookie_name = self._config.cookie_name
@@ -170,7 +165,7 @@
         web.setcookie(
             cookie_name,
             session_id,
-            expires=expires or self._config.timeout,
+            expires=expires,
             domain=cookie_domain,
             httponly=httponly,
             secure=secure,
@@ -317,7 +312,10 @@
             path = self._get_path(f)
             atime = os.stat(path).st_atime
             if now - atime > timeout:
-                shutil.rmtree(path)
+                if os.path.isdir(path):
+                    shutil.rmtree(path)
+                else:
+                    os.remove(path)
 
 
 class DBStore(Store):
@@ -432,8 +430,7 @@
         return key in self.d_store
 
     def __getitem__(self, key):
-        """ Return the value and update the last seen value
-        """
+        """Return the value and update the last seen value"""
         t, value = self.d_store[key]
         self.d_store[key] = (time.time(), value)
         return value
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/web.py-0.61/web/template.py 
new/web.py-0.62/web/template.py
--- old/web.py-0.61/web/template.py     2020-07-27 16:43:53.000000000 +0200
+++ new/web.py-0.62/web/template.py     2020-11-08 19:37:39.000000000 +0100
@@ -73,8 +73,7 @@
 
 
 class Parser:
-    """Parser Base.
-    """
+    """Parser Base."""
 
     def __init__(self):
         self.statement_nodes = STATEMENT_NODES
@@ -129,11 +128,11 @@
     def read_var(self, text):
         r"""Reads a var statement.
 
-            >>> read_var = Parser().read_var
-            >>> read_var('var x=10\nfoo')
-            (<var: x = 10>, 'foo')
-            >>> read_var('var x: hello $name\nfoo')
-            (<var: x = join_(u'hello ', escape_(name, True))>, 'foo')
+        >>> read_var = Parser().read_var
+        >>> read_var('var x=10\nfoo')
+        (<var: x = 10>, 'foo')
+        >>> read_var('var x: hello $name\nfoo')
+        (<var: x = join_(u'hello ', escape_(name, True))>, 'foo')
         """
         line, text = splitline(text)
         tokens = self.python_tokens(line)
@@ -167,9 +166,9 @@
     def read_suite(self, text):
         r"""Reads section by section till end of text.
 
-            >>> read_suite = Parser().read_suite
-            >>> read_suite('hello $name\nfoo\n')
-            [<line: [t'hello ', $name, t'\n']>, <line: [t'foo\n']>]
+        >>> read_suite = Parser().read_suite
+        >>> read_suite('hello $name\nfoo\n')
+        [<line: [t'hello ', $name, t'\n']>, <line: [t'foo\n']>]
         """
         sections = []
         while text:
@@ -180,13 +179,13 @@
     def readline(self, text):
         r"""Reads one line from the text. Newline is suppressed if the line 
ends with \.
 
-            >>> readline = Parser().readline
-            >>> readline('hello $name!\nbye!')
-            (<line: [t'hello ', $name, t'!\n']>, 'bye!')
-            >>> readline('hello $name!\\\nbye!')
-            (<line: [t'hello ', $name, t'!']>, 'bye!')
-            >>> readline('$f()\n\n')
-            (<line: [$f(), t'\n']>, '\n')
+        >>> readline = Parser().readline
+        >>> readline('hello $name!\nbye!')
+        (<line: [t'hello ', $name, t'!\n']>, 'bye!')
+        >>> readline('hello $name!\\\nbye!')
+        (<line: [t'hello ', $name, t'!']>, 'bye!')
+        >>> readline('$f()\n\n')
+        (<line: [$f(), t'\n']>, '\n')
         """
         line, text = splitline(text)
 
@@ -204,11 +203,11 @@
     def read_node(self, text):
         r"""Reads a node from the given text and returns the node and 
remaining text.
 
-            >>> read_node = Parser().read_node
-            >>> read_node('hello $name')
-            (t'hello ', '$name')
-            >>> read_node('$name')
-            ($name, '')
+        >>> read_node = Parser().read_node
+        >>> read_node('hello $name')
+        (t'hello ', '$name')
+        >>> read_node('$name')
+        ($name, '')
         """
         if text.startswith("$$"):
             return TextNode("$"), text[2:]
@@ -229,9 +228,9 @@
     def read_text(self, text):
         r"""Reads a text node from the given text.
 
-            >>> read_text = Parser().read_text
-            >>> read_text('hello $name')
-            (t'hello ', '$name')
+        >>> read_text = Parser().read_text
+        >>> read_text('hello $name')
+        (t'hello ', '$name')
         """
         index = text.find("$")
         if index < 0:
@@ -368,9 +367,9 @@
     def read_assignment(self, text):
         r"""Reads assignment statement from text.
 
-            >>> read_assignment = Parser().read_assignment
-            >>> read_assignment('a = b + 1\nfoo')
-            (<assignment: 'a = b + 1'>, 'foo')
+        >>> read_assignment = Parser().read_assignment
+        >>> read_assignment('a = b + 1\nfoo')
+        (<assignment: 'a = b + 1'>, 'foo')
         """
         line, text = splitline(text)
         return AssignmentNode(line.strip()), text
@@ -378,13 +377,13 @@
     def python_lookahead(self, text):
         """Returns the first python token from the given text.
 
-            >>> python_lookahead = Parser().python_lookahead
-            >>> python_lookahead('for i in range(10):')
-            'for'
-            >>> python_lookahead('else:')
-            'else'
-            >>> python_lookahead(' x = 1')
-            ' '
+        >>> python_lookahead = Parser().python_lookahead
+        >>> python_lookahead('for i in range(10):')
+        'for'
+        >>> python_lookahead('else:')
+        'else'
+        >>> python_lookahead(' x = 1')
+        ' '
         """
         i = iter([text])
         readline = lambda: next(i)
@@ -427,9 +426,9 @@
     def read_statement(self, text):
         r"""Reads a python statement.
 
-            >>> read_statement = Parser().read_statement
-            >>> read_statement('for i in range(10): hello $name')
-            ('for i in range(10):', ' hello $name')
+        >>> read_statement = Parser().read_statement
+        >>> read_statement('for i in range(10): hello $name')
+        ('for i in range(10):', ' hello $name')
         """
         tok = PythonTokenizer(text)
         tok.consume_till(":")
@@ -498,12 +497,12 @@
     def consume_till(self, delim):
         """Consumes tokens till colon.
 
-            >>> tok = PythonTokenizer('for i in range(10): hello $i')
-            >>> tok.consume_till(':')
-            >>> tok.text[:tok.index]
-            'for i in range(10):'
-            >>> tok.text[tok.index:]
-            ' hello $i'
+        >>> tok = PythonTokenizer('for i in range(10): hello $i')
+        >>> tok.consume_till(':')
+        >>> tok.text[:tok.index]
+        'for i in range(10):'
+        >>> tok.text[tok.index:]
+        ' hello $i'
         """
         try:
             while True:
@@ -829,8 +828,7 @@
 
 
 class ForLoopContext:
-    """Stackable context for ForLoop to support nested for loops.
-    """
+    """Stackable context for ForLoop to support nested for loops."""
 
     def __init__(self, forloop, parent):
         self._forloop = forloop
@@ -942,6 +940,13 @@
             builtins=builtins,
         )
 
+    def __repr__(self):
+        """
+        >>> Template(text='Template text', filename='burndown_chart.html')
+        <Template burndown_chart.html>
+        """
+        return "<{} {}>".format(self.__class__.__name__, self.filename)
+
     def normalize_text(text):
         """Normalizes template text by correcting \r\n, tabs and BOM chars."""
         text = text.replace("\r\n", "\n").replace("\r", "\n").expandtabs()
@@ -1086,9 +1091,8 @@
                 path, cache=self._cache is not None, base=self._base, 
**self._keywords
             )
         elif kind == "file":
-            return Template(
-                open(path, encoding="utf-8").read(), filename=path, 
**self._keywords
-            )
+            with open(path, encoding="utf-8") as tmpl_file:
+                return Template(tmpl_file.read(), filename=path, 
**self._keywords)
         else:
             raise AttributeError("No template named " + name)
 
@@ -1167,8 +1171,7 @@
 
 
 def frender(path, **keywords):
-    """Creates a template from the given file path.
-    """
+    """Creates a template from the given file path."""
     return Template(open(path, encoding="utf-8").read(), filename=path, 
**keywords)
 
 
@@ -1444,8 +1447,7 @@
         return self._d.keys()
 
     def _prepare_body(self):
-        """Prepare value of __body__ by joining parts.
-        """
+        """Prepare value of __body__ by joining parts."""
         if self._parts:
             value = u"".join(self._parts)
             self._parts[:] = []
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/web.py-0.61/web/utils.py new/web.py-0.62/web/utils.py
--- old/web.py-0.61/web/utils.py        2020-07-27 16:43:53.000000000 +0200
+++ new/web.py-0.62/web/utils.py        2020-11-08 19:37:39.000000000 +0100
@@ -204,19 +204,19 @@
 class Counter(storage):
     """Keeps count of how many times something is added.
 
-        >>> c = counter()
-        >>> c.add('x')
-        >>> c.add('x')
-        >>> c.add('x')
-        >>> c.add('x')
-        >>> c.add('x')
-        >>> c.add('y')
-        >>> c['y']
-        1
-        >>> c['x']
-        5
-        >>> c.most()
-        ['x']
+    >>> c = counter()
+    >>> c.add('x')
+    >>> c.add('x')
+    >>> c.add('x')
+    >>> c.add('x')
+    >>> c.add('x')
+    >>> c.add('y')
+    >>> c['y']
+    1
+    >>> c['x']
+    5
+    >>> c.most()
+    ['x']
     """
 
     def add(self, n):
@@ -236,51 +236,51 @@
     def percent(self, key):
         """Returns what percentage a certain key is of all entries.
 
-           >>> c = counter()
-           >>> c.add('x')
-           >>> c.add('x')
-           >>> c.add('x')
-           >>> c.add('y')
-           >>> c.percent('x')
-           0.75
-           >>> c.percent('y')
-           0.25
+        >>> c = counter()
+        >>> c.add('x')
+        >>> c.add('x')
+        >>> c.add('x')
+        >>> c.add('y')
+        >>> c.percent('x')
+        0.75
+        >>> c.percent('y')
+        0.25
         """
         return float(self[key]) / sum(self.values())
 
     def sorted_keys(self):
         """Returns keys sorted by value.
 
-             >>> c = counter()
-             >>> c.add('x')
-             >>> c.add('x')
-             >>> c.add('y')
-             >>> c.sorted_keys()
-             ['x', 'y']
+        >>> c = counter()
+        >>> c.add('x')
+        >>> c.add('x')
+        >>> c.add('y')
+        >>> c.sorted_keys()
+        ['x', 'y']
         """
         return sorted(self.keys(), key=lambda k: self[k], reverse=True)
 
     def sorted_values(self):
         """Returns values sorted by value.
 
-            >>> c = counter()
-            >>> c.add('x')
-            >>> c.add('x')
-            >>> c.add('y')
-            >>> c.sorted_values()
-            [2, 1]
+        >>> c = counter()
+        >>> c.add('x')
+        >>> c.add('x')
+        >>> c.add('y')
+        >>> c.sorted_values()
+        [2, 1]
         """
         return [self[k] for k in self.sorted_keys()]
 
     def sorted_items(self):
         """Returns items sorted by value.
 
-            >>> c = counter()
-            >>> c.add('x')
-            >>> c.add('x')
-            >>> c.add('y')
-            >>> c.sorted_items()
-            [('x', 2), ('y', 1)]
+        >>> c = counter()
+        >>> c.add('x')
+        >>> c.add('x')
+        >>> c.add('y')
+        >>> c.sorted_items()
+        [('x', 2), ('y', 1)]
         """
         return [(k, self[k]) for k in self.sorted_keys()]
 
@@ -736,8 +736,7 @@
 
 
 def safeiter(it, cleanup=None, ignore_errors=True):
-    """Makes an iterator safe by ignoring the exceptions occurred during the 
iteration.
-    """
+    """Makes an iterator safe by ignoring the exceptions occurred during the 
iteration."""
 
     def next():
         while True:
@@ -842,11 +841,11 @@
 def requeue(queue, index=-1):
     """Returns the element at index after moving it to the beginning of the 
queue.
 
-        >>> x = [1, 2, 3, 4]
-        >>> requeue(x)
-        4
-        >>> x
-        [4, 1, 2, 3]
+    >>> x = [1, 2, 3, 4]
+    >>> requeue(x)
+    4
+    >>> x
+    [4, 1, 2, 3]
     """
     x = queue.pop(index)
     queue.insert(0, x)
@@ -856,11 +855,11 @@
 def restack(stack, index=0):
     """Returns the element at index after moving it to the top of stack.
 
-           >>> x = [1, 2, 3, 4]
-           >>> restack(x)
-           1
-           >>> x
-           [2, 3, 4, 1]
+    >>> x = [1, 2, 3, 4]
+    >>> restack(x)
+    1
+    >>> x
+    [2, 3, 4, 1]
     """
     x = stack.pop(index)
     stack.append(x)
@@ -1282,8 +1281,7 @@
         return id(self)
 
     def clear_all():
-        """Clears all ThreadedDict instances.
-        """
+        """Clears all ThreadedDict instances."""
         for t in list(ThreadedDict._instances):
             t.clear()
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/web.py-0.61/web/webapi.py 
new/web.py-0.62/web/webapi.py
--- old/web.py-0.61/web/webapi.py       2020-07-27 16:43:53.000000000 +0200
+++ new/web.py-0.62/web/webapi.py       2020-11-08 19:37:39.000000000 +0100
@@ -237,8 +237,7 @@
 
 
 def NotFound(message=None):
-    """Returns HTTPError with '404 Not Found' error from the active 
application.
-    """
+    """Returns HTTPError with '404 Not Found' error from the active 
application."""
     if message:
         return _NotFound(message)
     elif ctx.get("app_stack"):
@@ -353,8 +352,7 @@
 
 
 def UnavailableForLegalReasons(message=None):
-    """Returns HTTPError with '415 Unavailable For Legal Reasons' error from 
the active application.
-    """
+    """Returns HTTPError with '415 Unavailable For Legal Reasons' error from 
the active application."""
     if message:
         return _UnavailableForLegalReasons(message)
     elif ctx.get("app_stack"):
@@ -378,8 +376,7 @@
 
 
 def InternalError(message=None):
-    """Returns HTTPError with '500 internal error' error from the active 
application.
-    """
+    """Returns HTTPError with '500 internal error' error from the active 
application."""
     if message:
         return _InternalError(message)
     elif ctx.get("app_stack"):
@@ -428,8 +425,7 @@
 
 
 def rawinput(method=None):
-    """Returns storage object with GET or POST arguments.
-    """
+    """Returns storage object with GET or POST arguments."""
     method = method or "both"
 
     def dictify(fs):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/web.py-0.61/web.py.egg-info/PKG-INFO 
new/web.py-0.62/web.py.egg-info/PKG-INFO
--- old/web.py-0.61/web.py.egg-info/PKG-INFO    2020-07-27 17:34:14.000000000 
+0200
+++ new/web.py-0.62/web.py.egg-info/PKG-INFO    2020-11-09 12:20:51.000000000 
+0100
@@ -1,6 +1,6 @@
 Metadata-Version: 2.1
 Name: web.py
-Version: 0.61
+Version: 0.62
 Summary: web.py: makes web apps
 Home-page: http://webpy.org/
 Author: Aaron Swartz
@@ -15,11 +15,11 @@
         [![build 
status](https://secure.travis-ci.org/webpy/webpy.png?branch=master)](https://travis-ci.org/webpy/webpy)
         [![Codecov Test 
Coverage](https://codecov.io/gh/webpy/webpy/branch/master/graphs/badge.svg?style=flat)](https://codecov.io/gh/webpy/webpy)
         
-        The latest stable release `0.61` only supports Python >= 3.5.
+        The latest stable release `0.62` only supports Python >= 3.5.
         To install it, please run:
         ```
         # For Python 3
-        python3 -m pip install web.py==0.61
+        python3 -m pip install web.py==0.62
         ```
         
         If you are still using Python 2.7, then please use web.py version 0.51
@@ -32,12 +32,13 @@
         You can also download it from [GitHub 
Releases](https://github.com/webpy/webpy/releases)
         page, then install it manually:
         ```
-        unzip webpy-0.61.zip
-        cd webpy-0.61/
+        unzip webpy-0.62.zip
+        cd webpy-0.62/
         python3 setup.py install
         ```
         
         Note: `0.5x` (e.g. 0.50, 0.51) are our last releases which support 
Python 2.
+        Note: `0.6x` (e.g. 0.60, 0.61, 0.62) are our last releases which 
support Python 3.5.
         
 Platform: any
 Classifier: License :: Public Domain
@@ -47,5 +48,6 @@
 Classifier: Programming Language :: Python :: 3.6
 Classifier: Programming Language :: Python :: 3.7
 Classifier: Programming Language :: Python :: 3.8
+Classifier: Programming Language :: Python :: 3.9
 Requires-Python: >=3.5
 Description-Content-Type: text/markdown


Reply via email to