1.1 --- a/.hgtags Wed Jan 28 11:40:31 2015 +0100
1.2 +++ b/.hgtags Wed Jan 28 11:44:22 2015 +0100
1.3 @@ -2,3 +2,4 @@
1.4 3d28b3c1896e7569c430662c5d4ec0b536266d56 rel-0-2
1.5 52ce79af5ae73b95addbe67b88f2f1037219246e rel-0-3
1.6 91c146800e069c86f592798b63bf4bdb4b7be271 rel-0-4
1.7 +c9b3086a024c406ab8386b9f96e04c1f76cf1c13 rel-0-4-1
2.1 --- a/DateSupport.py Wed Jan 28 11:40:31 2015 +0100
2.2 +++ b/DateSupport.py Wed Jan 28 11:44:22 2015 +0100
2.3 @@ -2,7 +2,7 @@
2.4 """
2.5 MoinMoin - DateSupport library (derived from EventAggregatorSupport)
2.6
2.7 - @copyright: 2008, 2009, 2010, 2011, 2012, 2013 by Paul Boddie <paul@boddie.org.uk>
2.8 + @copyright: 2008, 2009, 2010, 2011, 2012, 2013, 2014 by Paul Boddie <paul@boddie.org.uk>
2.9 @license: GNU GPL (v2 or later), see COPYING.txt for details.
2.10 """
2.11
2.12 @@ -58,7 +58,7 @@
2.13 timezone_iso8601_offset_str = ur'(?P<offset>(?:(?P<sign>[-+])(?P<hours>[0-9]{2}):(?P<minutes>[0-9]{2})))'
2.14 datetime_iso8601_regexp_str = date_regexp_str + \
2.15 ur'(?:T' + time_regexp_str + \
2.16 - ur'(?:(?P<utc>Z)|(?P<zone>' + timezone_iso8601_offset_str + '))' \
2.17 + ur'(?:(?P<utc>Z)|(?P<zone>' + timezone_iso8601_offset_str + '))?' \
2.18 ur')?'
2.19
2.20 date_icalendar_regexp = re.compile(date_icalendar_regexp_str, re.UNICODE)
2.21 @@ -392,7 +392,7 @@
2.22 def __str__(self):
2.23 return Date.__str__(self) + self.time_string()
2.24
2.25 - def time_string(self, zone_as_offset=False, time_prefix=" ", zone_prefix=" "):
2.26 + def time_string(self, zone_as_offset=False, time_prefix=" ", zone_prefix=" ", zone_separator=":"):
2.27 if self.has_time():
2.28 data = self.as_tuple()
2.29 time_str = "%s%02d:%02d" % ((time_prefix,) + data[3:5])
2.30 @@ -402,13 +402,23 @@
2.31 if zone_as_offset:
2.32 utc_offset = self.utc_offset()
2.33 if utc_offset:
2.34 - time_str += "%s%+03d:%02d" % ((zone_prefix,) + utc_offset)
2.35 + time_str += "%s%+03d%s%02d" % (zone_prefix, utc_offset[0], zone_separator, utc_offset[1])
2.36 else:
2.37 time_str += "%s%s" % (zone_prefix, data[6])
2.38 return time_str
2.39 else:
2.40 return ""
2.41
2.42 + def as_RFC2822_datetime_string(self):
2.43 + weekday = calendar.weekday(*self.data[:3])
2.44 + return "%s, %02d %s %04d %s" % (
2.45 + getDayLabel(weekday)[:3],
2.46 + self.data[2],
2.47 + getMonthLabel(self.data[1])[:3],
2.48 + self.data[0],
2.49 + self.time_string(zone_as_offset=True, time_prefix="", zone_prefix=" ", zone_separator="")
2.50 + )
2.51 +
2.52 def as_HTTP_datetime_string(self):
2.53 weekday = calendar.weekday(*self.data[:3])
2.54 return "%s, %02d %s %04d %02d:%02d:%02d GMT" % ((
3.1 --- a/ItemSupport.py Wed Jan 28 11:40:31 2015 +0100
3.2 +++ b/ItemSupport.py Wed Jan 28 11:44:22 2015 +0100
3.3 @@ -2,135 +2,60 @@
3.4 """
3.5 MoinMoin - ItemSupport library
3.6
3.7 - @copyright: 2013 by Paul Boddie <paul@boddie.org.uk>
3.8 + @copyright: 2013, 2014 by Paul Boddie <paul@boddie.org.uk>
3.9 @license: GNU GPL (v2 or later), see COPYING.txt for details.
3.10 """
3.11
3.12 +from MoinMoin import config
3.13 +from MoinMoin.Page import Page
3.14 +from MoinMoin.PageEditor import PageEditor
3.15 +from MoinMoin.security import Permissions
3.16 from MoinMoin.util import lock
3.17 +from MoinSupport import getMetadata, getPagesForSearch
3.18 import os
3.19
3.20 # Content storage support.
3.21
3.22 -class ItemDirectoryStore:
3.23 +class SpecialPermissionsForPage(Permissions):
3.24 +
3.25 + "Permit saving of ACL-enabled pages."
3.26 +
3.27 + def __init__(self, user, pagename):
3.28 + Permissions.__init__(self, user)
3.29 + self.pagename = pagename
3.30
3.31 - "A directory-based item store."
3.32 + def admin(self, pagename):
3.33 + return pagename == self.pagename
3.34 +
3.35 + write = admin
3.36 +
3.37 +class ReadPermissionsForSubpages(Permissions):
3.38 +
3.39 + "Permit listing of ACL-affected subpages."
3.40
3.41 - def __init__(self, path, lock_dir):
3.42 + def __init__(self, user, pagename):
3.43 + Permissions.__init__(self, user)
3.44 + self.pagename = pagename
3.45 +
3.46 + def read(self, pagename):
3.47 + return pagename.startswith("%s/" % self.pagename)
3.48 +
3.49 +# Underlying storage mechanisms.
3.50
3.51 - "Initialise an item store for the given 'path' and 'lock_dir'."
3.52 +class GeneralItemStore:
3.53 +
3.54 + "Common item store functionality."
3.55
3.56 - self.path = path
3.57 - self.next_path = os.path.join(self.path, "next")
3.58 + def __init__(self, lock_dir):
3.59 +
3.60 + "Initialise an item store with the given 'lock_dir' guarding access."
3.61 +
3.62 self.lock_dir = lock_dir
3.63 self.writelock = lock.WriteLock(lock_dir)
3.64 self.readlock = lock.ReadLock(lock_dir)
3.65
3.66 - def mtime(self):
3.67 -
3.68 - "Return the last modified time of the item store directory."
3.69 -
3.70 - return os.path.getmtime(self.path)
3.71 -
3.72 - def get_next(self):
3.73 -
3.74 - "Return the next item number."
3.75 -
3.76 - next = self.read_next()
3.77 - if next is None:
3.78 - next = self.deduce_next()
3.79 - self.write_next(next)
3.80 - return next
3.81 -
3.82 - def get_keys(self):
3.83 -
3.84 - "Return the item keys."
3.85 -
3.86 - return [int(filename) for filename in os.listdir(self.path) if filename.isdigit()]
3.87 -
3.88 - def deduce_next(self):
3.89 -
3.90 - "Deduce the next item number from the existing item files."
3.91 -
3.92 - return max(self.get_keys() or [-1]) + 1
3.93 -
3.94 - def read_next(self):
3.95 -
3.96 - "Read the next item number from a special file."
3.97 -
3.98 - if not os.path.exists(self.next_path):
3.99 - return None
3.100 -
3.101 - f = open(self.next_path)
3.102 - try:
3.103 - try:
3.104 - return int(f.read())
3.105 - except ValueError:
3.106 - return None
3.107 - finally:
3.108 - f.close()
3.109 -
3.110 - def write_next(self, next):
3.111 -
3.112 - "Write the 'next' item number to a special file."
3.113 -
3.114 - f = open(self.next_path, "w")
3.115 - try:
3.116 - f.write(str(next))
3.117 - finally:
3.118 - f.close()
3.119 -
3.120 - def write_item(self, item, next):
3.121 -
3.122 - "Write the given 'item' to a file with the given 'next' item number."
3.123 -
3.124 - f = open(self.get_item_path(next), "w")
3.125 - try:
3.126 - f.write(item)
3.127 - finally:
3.128 - f.close()
3.129 -
3.130 - def read_item(self, number):
3.131 -
3.132 - "Read the item with the given item 'number'."
3.133 -
3.134 - f = open(self.get_item_path(number))
3.135 - try:
3.136 - return f.read()
3.137 - finally:
3.138 - f.close()
3.139 -
3.140 - def remove_item(self, number):
3.141 -
3.142 - "Remove the item with the given item 'number'."
3.143 -
3.144 - os.remove(self.get_item_path(number))
3.145 -
3.146 - def get_item_path(self, number):
3.147 -
3.148 - "Get the path for the given item 'number'."
3.149 -
3.150 - path = os.path.abspath(os.path.join(self.path, str(number)))
3.151 - basepath = os.path.join(self.path, "")
3.152 -
3.153 - if os.path.commonprefix([path, basepath]) != basepath:
3.154 - raise OSError, path
3.155 -
3.156 - return path
3.157 -
3.158 # High-level methods.
3.159
3.160 - def append(self, item):
3.161 -
3.162 - "Append the given 'item' to the store."
3.163 -
3.164 - self.writelock.acquire()
3.165 - try:
3.166 - next = self.get_next()
3.167 - self.write_item(item, next)
3.168 - self.write_next(next + 1)
3.169 - finally:
3.170 - self.writelock.release()
3.171 -
3.172 def __len__(self):
3.173
3.174 """
3.175 @@ -194,13 +119,280 @@
3.176 finally:
3.177 self.writelock.release()
3.178
3.179 +class SequentialAccess(GeneralItemStore):
3.180 +
3.181 + "Support sequential access to items."
3.182 +
3.183 + def deduce_next(self):
3.184 +
3.185 + "Deduce the next item number from the existing item files."
3.186 +
3.187 + return max(self.get_keys() or [-1]) + 1
3.188 +
3.189 + def read_next(self):
3.190 +
3.191 + "Read the next item number from a special file."
3.192 +
3.193 + if not os.path.exists(self.next_path):
3.194 + return None
3.195 +
3.196 + f = open(self.next_path)
3.197 + try:
3.198 + try:
3.199 + return int(f.read())
3.200 + except ValueError:
3.201 + return None
3.202 + finally:
3.203 + f.close()
3.204 +
3.205 + def write_next(self, next):
3.206 +
3.207 + "Write the 'next' item number to a special file."
3.208 +
3.209 + f = open(self.next_path, "w")
3.210 + try:
3.211 + f.write(str(next))
3.212 + finally:
3.213 + f.close()
3.214 +
3.215 +class DirectoryStore(GeneralItemStore):
3.216 +
3.217 + "A directory-based item store."
3.218 +
3.219 + def __init__(self, path, lock_dir):
3.220 +
3.221 + "Initialise an item store for the given 'path' and 'lock_dir'."
3.222 +
3.223 + self.path = path
3.224 + self.next_path = os.path.join(self.path, "next")
3.225 + self.lock_dir = lock_dir
3.226 + self.writelock = lock.WriteLock(lock_dir)
3.227 + self.readlock = lock.ReadLock(lock_dir)
3.228 +
3.229 + def mtime(self):
3.230 +
3.231 + "Return the last modified time of the item store directory."
3.232 +
3.233 + return os.path.getmtime(self.path)
3.234 +
3.235 + def get_keys(self):
3.236 +
3.237 + "Return the item keys."
3.238 +
3.239 + return [int(filename) for filename in os.listdir(self.path) if filename.isdigit()]
3.240 +
3.241 + def write_item(self, item, next):
3.242 +
3.243 + "Write the given 'item' to a file with the given 'next' item number."
3.244 +
3.245 + f = open(self.get_item_path(next), "wb")
3.246 + try:
3.247 + f.write(item)
3.248 + finally:
3.249 + f.close()
3.250 +
3.251 + def read_item(self, identifier):
3.252 +
3.253 + "Read the item with the given item 'identifier'."
3.254 +
3.255 + f = open(self.get_item_path(identifier), "rb")
3.256 + try:
3.257 + return f.read()
3.258 + finally:
3.259 + f.close()
3.260 +
3.261 + def remove_item(self, identifier):
3.262 +
3.263 + "Remove the item with the given item 'identifier'."
3.264 +
3.265 + os.remove(self.get_item_path(identifier))
3.266 +
3.267 + def get_item_path(self, identifier):
3.268 +
3.269 + "Get the path for the given item 'identifier'."
3.270 +
3.271 + if isinstance(identifier, unicode):
3.272 + filename = identifier.encode(config.charset)
3.273 + else:
3.274 + filename = identifier
3.275 +
3.276 + path = os.path.abspath(os.path.join(self.path, filename))
3.277 + basepath = os.path.join(self.path, "")
3.278 +
3.279 + if os.path.commonprefix([path, basepath]) != basepath:
3.280 + raise OSError, path
3.281 +
3.282 + return path
3.283 +
3.284 +class DirectoryItemStore(DirectoryStore, SequentialAccess):
3.285 +
3.286 + "A directory-based item store with numeric keys."
3.287 +
3.288 + def get_next(self):
3.289 +
3.290 + "Return the next item number."
3.291 +
3.292 + next = self.read_next()
3.293 + if next is None:
3.294 + next = self.deduce_next()
3.295 + self.write_next(next)
3.296 + return next
3.297 +
3.298 + # High-level methods.
3.299 +
3.300 + def append(self, item):
3.301 +
3.302 + "Append the given 'item' to the store."
3.303 +
3.304 + self.writelock.acquire()
3.305 + try:
3.306 + next = self.get_next()
3.307 + self.write_item(item, next)
3.308 + self.write_next(next + 1)
3.309 + finally:
3.310 + self.writelock.release()
3.311 +
3.312 +class DirectoryNamedItemStore(DirectoryStore):
3.313 +
3.314 + "A directory-based item store with explicit keys."
3.315 +
3.316 + def __setitem__(self, name, item):
3.317 +
3.318 + "Using the given 'name', set the given 'item' in the store."
3.319 +
3.320 + self.writelock.acquire()
3.321 + try:
3.322 + self.write_item(item, name)
3.323 + finally:
3.324 + self.writelock.release()
3.325 +
3.326 +class SubpageItemStore(SequentialAccess):
3.327 +
3.328 + "A subpage-based item store."
3.329 +
3.330 + def __init__(self, page, lock_dir):
3.331 +
3.332 + "Initialise an item store for subpages under the given 'page'."
3.333 +
3.334 + GeneralItemStore.__init__(self, lock_dir)
3.335 + self.page = page
3.336 +
3.337 + def mtime(self):
3.338 +
3.339 + "Return the last modified time of the item store."
3.340 +
3.341 + keys = self.get_keys()
3.342 + if not keys:
3.343 + page = self.page
3.344 + else:
3.345 + page = Page(self.page.request, self.get_item_path(max(keys)))
3.346 +
3.347 + return wikiutil.version2timestamp(
3.348 + getMetadata(page)["last-modified"]
3.349 + )
3.350 +
3.351 + def get_next(self):
3.352 +
3.353 + "Return the next item number."
3.354 +
3.355 + return self.deduce_next()
3.356 +
3.357 + def get_keys(self):
3.358 +
3.359 + "Return the item keys."
3.360 +
3.361 + request = self.page.request
3.362 +
3.363 + # Collect the strict subpages of the parent page.
3.364 +
3.365 + leafnames = []
3.366 + parentname = self.page.page_name
3.367 +
3.368 + # To list pages whose ACLs may prevent access, a special policy is required.
3.369 +
3.370 + may = request.user.may
3.371 + request.user.may = ReadPermissionsForSubpages(request.user, parentname)
3.372 +
3.373 + try:
3.374 + for page in getPagesForSearch("title:regex:^%s/" % parentname, self.page.request):
3.375 + basename, leafname = page.page_name.rsplit("/", 1)
3.376 +
3.377 + # Only collect numbered pages immediately below the parent.
3.378 +
3.379 + if basename == parentname and leafname.isdigit():
3.380 + leafnames.append(int(leafname))
3.381 +
3.382 + return leafnames
3.383 +
3.384 + # Restore the original policy.
3.385 +
3.386 + finally:
3.387 + request.user.may = may
3.388 +
3.389 + def write_item(self, item, next):
3.390 +
3.391 + "Write the given 'item' to a page with the given 'next' item number."
3.392 +
3.393 + request = self.page.request
3.394 + pagename = self.get_item_path(next)
3.395 +
3.396 + # To add a page with an ACL, a special policy is required.
3.397 +
3.398 + may = request.user.may
3.399 + request.user.may = SpecialPermissionsForPage(request.user, pagename)
3.400 +
3.401 + # Attempt to save the page, copying any ACL.
3.402 +
3.403 + try:
3.404 + page = PageEditor(request, pagename)
3.405 + page.saveText(item, 0)
3.406 +
3.407 + # Restore the original policy.
3.408 +
3.409 + finally:
3.410 + request.user.may = may
3.411 +
3.412 + def read_item(self, number):
3.413 +
3.414 + "Read the item with the given item 'number'."
3.415 +
3.416 + page = Page(self.page.request, self.get_item_path(number))
3.417 + return page.get_raw_body()
3.418 +
3.419 + def remove_item(self, number):
3.420 +
3.421 + "Remove the item with the given item 'number'."
3.422 +
3.423 + page = PageEditor(self.page.request, self.get_item_path(number))
3.424 + page.deletePage()
3.425 +
3.426 + def get_item_path(self, number):
3.427 +
3.428 + "Get the path for the given item 'number'."
3.429 +
3.430 + return "%s/%s" % (self.page.page_name, number)
3.431 +
3.432 + # High-level methods.
3.433 +
3.434 + def append(self, item):
3.435 +
3.436 + "Append the given 'item' to the store."
3.437 +
3.438 + self.writelock.acquire()
3.439 + try:
3.440 + next = self.get_next()
3.441 + self.write_item(item, next)
3.442 + finally:
3.443 + self.writelock.release()
3.444 +
3.445 class ItemIterator:
3.446
3.447 "An iterator over items in a store."
3.448
3.449 - def __init__(self, store, direction=1):
3.450 + def __init__(self, store, direction=1, keys=None):
3.451 self.store = store
3.452 self.direction = direction
3.453 + self.keys = keys
3.454 self.reset()
3.455
3.456 def reset(self):
3.457 @@ -218,7 +410,10 @@
3.458 return self._next >= self.final
3.459
3.460 def get_next(self):
3.461 - next = self._next
3.462 + if self.keys:
3.463 + next = self.keys[self._next]
3.464 + else:
3.465 + next = self._next
3.466 self._next += self.direction
3.467 return next
3.468
3.469 @@ -242,4 +437,174 @@
3.470 def __iter__(self):
3.471 return self
3.472
3.473 +def getDirectoryItemStoreForPage(page, item_dir, lock_dir):
3.474 +
3.475 + """
3.476 + A convenience function returning a directory-based store for the given
3.477 + 'page', using the given 'item_dir' and 'lock_dir'.
3.478 + """
3.479 +
3.480 + item_dir_path = tuple(item_dir.split("/"))
3.481 + lock_dir_path = tuple(lock_dir.split("/"))
3.482 + return DirectoryItemStore(page.getPagePath(*item_dir_path), page.getPagePath(*lock_dir_path))
3.483 +
3.484 +def getDirectoryNamedItemStoreForPage(page, item_dir, lock_dir):
3.485 +
3.486 + """
3.487 + A convenience function returning a directory-based store for the given
3.488 + 'page', using the given 'item_dir' and 'lock_dir'.
3.489 + """
3.490 +
3.491 + item_dir_path = tuple(item_dir.split("/"))
3.492 + lock_dir_path = tuple(lock_dir.split("/"))
3.493 + return DirectoryNamedItemStore(page.getPagePath(*item_dir_path), page.getPagePath(*lock_dir_path))
3.494 +
3.495 +def getSubpageItemStoreForPage(page, lock_dir):
3.496 +
3.497 + """
3.498 + A convenience function returning a subpage-based store for the given
3.499 + 'page', using the given 'lock_dir'.
3.500 + """
3.501 +
3.502 + lock_dir_path = tuple(lock_dir.split("/"))
3.503 + return SubpageItemStore(page, page.getPagePath(*lock_dir_path))
3.504 +
3.505 +# Page-oriented item store classes.
3.506 +
3.507 +class ItemStoreBase:
3.508 +
3.509 + "Access item stores via pages, observing page access restrictions."
3.510 +
3.511 + def __init__(self, page, store):
3.512 + self.page = page
3.513 + self.store = store
3.514 +
3.515 + def can_write(self):
3.516 +
3.517 + """
3.518 + Return whether the user associated with the request can write to the
3.519 + page owning this store.
3.520 + """
3.521 +
3.522 + user = self.page.request.user
3.523 + return user and user.may.write(self.page.page_name)
3.524 +
3.525 + def can_read(self):
3.526 +
3.527 + """
3.528 + Return whether the user associated with the request can read from the
3.529 + page owning this store.
3.530 + """
3.531 +
3.532 + user = self.page.request.user
3.533 + return user and user.may.read(self.page.page_name)
3.534 +
3.535 + def can_delete(self):
3.536 +
3.537 + """
3.538 + Return whether the user associated with the request can delete the
3.539 + page owning this store.
3.540 + """
3.541 +
3.542 + user = self.page.request.user
3.543 + return user and user.may.delete(self.page.page_name)
3.544 +
3.545 + # Store-specific methods.
3.546 +
3.547 + def mtime(self):
3.548 + return self.store.mtime()
3.549 +
3.550 + # High-level methods.
3.551 +
3.552 + def keys(self):
3.553 +
3.554 + "Return a list of keys for items in the store."
3.555 +
3.556 + if not self.can_read():
3.557 + return 0
3.558 +
3.559 + return self.store.keys()
3.560 +
3.561 + def __len__(self):
3.562 +
3.563 + "Return the number of items in the store."
3.564 +
3.565 + if not self.can_read():
3.566 + return 0
3.567 +
3.568 + return len(self.store)
3.569 +
3.570 + def __getitem__(self, number):
3.571 +
3.572 + "Return the item with the given 'number'."
3.573 +
3.574 + if not self.can_read():
3.575 + raise IndexError, number
3.576 +
3.577 + return self.store.__getitem__(number)
3.578 +
3.579 + def __delitem__(self, number):
3.580 +
3.581 + "Remove the item with the given 'number'."
3.582 +
3.583 + if not self.can_delete():
3.584 + return
3.585 +
3.586 + return self.store.__delitem__(number)
3.587 +
3.588 + def __iter__(self):
3.589 + return self.store.__iter__()
3.590 +
3.591 + def next(self):
3.592 + return self.store.next()
3.593 +
3.594 +class SequentialStoreBase:
3.595 +
3.596 + "Sequential access methods for item stores."
3.597 +
3.598 + def append(self, item):
3.599 +
3.600 + "Append the given 'item' to the store."
3.601 +
3.602 + if not self.can_write():
3.603 + return
3.604 +
3.605 + self.store.append(item)
3.606 +
3.607 +class NamedStoreBase:
3.608 +
3.609 + "Name-based access methods for item stores."
3.610 +
3.611 + def __setitem__(self, name, item):
3.612 +
3.613 + "Using the given 'name', set the given 'item' in the store."
3.614 +
3.615 + if not self.can_write():
3.616 + return
3.617 +
3.618 + self.store[name] = item
3.619 +
3.620 +# Convenience store classes.
3.621 +
3.622 +class ItemStore(ItemStoreBase, SequentialStoreBase):
3.623 +
3.624 + "Store items in a directory via a page."
3.625 +
3.626 + def __init__(self, page, item_dir="items", lock_dir=None):
3.627 + ItemStoreBase.__init__(self, page, getDirectoryItemStoreForPage(page, item_dir, lock_dir or ("%s-locks" % item_dir)))
3.628 +
3.629 +class NamedItemStore(ItemStoreBase, NamedStoreBase):
3.630 +
3.631 + "Store items in a directory via a page."
3.632 +
3.633 + def __init__(self, page, item_dir="items", lock_dir=None):
3.634 + ItemStoreBase.__init__(self, page, getDirectoryNamedItemStoreForPage(page, item_dir, lock_dir or ("%s-locks" % item_dir)))
3.635 +
3.636 +class ItemSubpageStore(ItemStoreBase):
3.637 +
3.638 + "Store items in subpages of a page."
3.639 +
3.640 + def __init__(self, page, lock_dir=None):
3.641 + ItemStoreBase.__init__(self, page, getSubpageItemStoreForPage(page, lock_dir or "subpage-items-locks"))
3.642 +
3.643 # vim: tabstop=4 expandtab shiftwidth=4
4.1 --- a/MoinRemoteSupport.py Wed Jan 28 11:40:31 2015 +0100
4.2 +++ b/MoinRemoteSupport.py Wed Jan 28 11:44:22 2015 +0100
4.3 @@ -2,16 +2,23 @@
4.4 """
4.5 MoinMoin - MoinRemoteSupport library
4.6
4.7 - @copyright: 2011, 2012, 2013 by Paul Boddie <paul@boddie.org.uk>
4.8 + @copyright: 2011, 2012, 2013, 2014 by Paul Boddie <paul@boddie.org.uk>
4.9 @license: GNU GPL (v2 or later), see COPYING.txt for details.
4.10 """
4.11
4.12 from ContentTypeSupport import getContentTypeAndEncoding
4.13 from MoinMoin.action import cache
4.14 -from MoinMoin import caching
4.15 +from MoinMoin import caching, log
4.16 +from email.parser import Parser
4.17 +from email.mime.multipart import MIMEMultipart
4.18 +from urllib import splithost, splitpasswd, splitport, splituser, unquote_plus
4.19 +from urlparse import urlsplit
4.20 import urllib2, time
4.21 +import imaplib
4.22
4.23 -def getCachedResource(request, url, arena, scope, max_cache_age):
4.24 +logging = log.getLogger(__name__)
4.25 +
4.26 +def getCachedResource(request, url, arena, scope, max_cache_age, reader=None):
4.27
4.28 """
4.29 Using the given 'request', return the resource data for the given 'url',
4.30 @@ -19,6 +26,10 @@
4.31 has already been downloaded. The 'max_cache_age' indicates the length in
4.32 seconds that a cache entry remains valid.
4.33
4.34 + If the optional 'reader' object is given, it will be used to access the
4.35 + 'url' and write the downloaded data to a cache entry. Otherwise, a standard
4.36 + URL reader will be used.
4.37 +
4.38 If the resource cannot be downloaded and cached, None is returned.
4.39 Otherwise, the form of the data is as follows:
4.40
4.41 @@ -29,6 +40,8 @@
4.42 content-body
4.43 """
4.44
4.45 + reader = reader or urlreader
4.46 +
4.47 # See if the URL is cached.
4.48
4.49 cache_key = cache.key(request, content=url)
4.50 @@ -43,32 +56,27 @@
4.51 # NOTE: The URL could be checked and the 'If-Modified-Since' header
4.52 # NOTE: (see MoinMoin.action.pollsistersites) could be checked.
4.53
4.54 - if not cache_entry.exists() or now - mtime >= max_cache_age:
4.55 + if not cache_entry.exists() or cache_entry.size() == 0 or now - mtime >= max_cache_age:
4.56
4.57 # Access the remote data source.
4.58
4.59 cache_entry.open(mode="w")
4.60
4.61 try:
4.62 - f = urllib2.urlopen(url)
4.63 try:
4.64 - cache_entry.write(url + "\n")
4.65 - cache_entry.write((f.headers.get("content-type") or "") + "\n")
4.66 - for key, value in f.headers.items():
4.67 - if key.lower() != "content-type":
4.68 - cache_entry.write("%s: %s\n" % (key, value))
4.69 - cache_entry.write("\n")
4.70 - cache_entry.write(f.read())
4.71 - finally:
4.72 - cache_entry.close()
4.73 - f.close()
4.74 + # Read from the source and write to the cache.
4.75 +
4.76 + reader(url, cache_entry)
4.77 +
4.78 + # In case of an exception, return None.
4.79
4.80 - # In case of an exception, return None.
4.81 + except IOError:
4.82 + if cache_entry.exists():
4.83 + cache_entry.remove()
4.84 + return None
4.85
4.86 - except IOError:
4.87 - if cache_entry.exists():
4.88 - cache_entry.remove()
4.89 - return None
4.90 + finally:
4.91 + cache_entry.close()
4.92
4.93 # Open the cache entry and read it.
4.94
4.95 @@ -78,6 +86,111 @@
4.96 finally:
4.97 cache_entry.close()
4.98
4.99 +def urlreader(url, cache_entry):
4.100 +
4.101 + "Retrieve data from the given 'url', writing it to the 'cache_entry'."
4.102 +
4.103 + f = urllib2.urlopen(url)
4.104 + try:
4.105 + writeCacheHeaders(url, f.headers, cache_entry)
4.106 + cache_entry.write(f.read())
4.107 + finally:
4.108 + f.close()
4.109 +
4.110 +def imapreader(url, cache_entry):
4.111 +
4.112 + """
4.113 + Retrieve data associated with the given 'url' using the IMAP protocol
4.114 + specifically, writing it to the 'cache_entry'.
4.115 + """
4.116 +
4.117 + # NOTE: Should use something like pykolab.imap_utf7.encode here.
4.118 +
4.119 + enc = lambda s: s.encode("utf-7")
4.120 +
4.121 + # The URL maps to credentials and folder details.
4.122 +
4.123 + scheme, netloc, path, query, fragment = urlsplit(url)
4.124 + credentials, location = splituser(netloc)
4.125 + username, password = map(unquote_plus, splitpasswd(credentials))
4.126 + host, port = splitport(location)
4.127 + folders = map(unquote_plus, path.split("/")[1:])
4.128 +
4.129 + # Connect and log in to the IMAP server.
4.130 +
4.131 + cls = scheme == "imaps" and imaplib.IMAP4_SSL or imaplib.IMAP4
4.132 +
4.133 + if port is None:
4.134 + i = cls(host)
4.135 + else:
4.136 + i = cls(host, int(port))
4.137 +
4.138 + i.login(username, password)
4.139 +
4.140 + try:
4.141 + # Descend to the desired folder.
4.142 +
4.143 + for folder in folders:
4.144 + code, response = i.select(enc(folder), readonly=True)
4.145 + if code != "OK":
4.146 + logging.warning("Could not enter folder: %s" % folder)
4.147 + raise IOError
4.148 +
4.149 + # Search for all messages.
4.150 + # NOTE: This could also be parameterised.
4.151 +
4.152 + code, response = i.search(None, "(ALL)")
4.153 +
4.154 + if code != "OK":
4.155 + logging.warning("Could not enter folder: %s" % folder)
4.156 + raise IOError
4.157 +
4.158 + # For each result, obtain the full message, but embed it in a larger
4.159 + # multipart message.
4.160 +
4.161 + message = MIMEMultipart()
4.162 +
4.163 + writeCacheHeaders(url, message, cache_entry)
4.164 +
4.165 + numbers = response and response[0].split(" ") or []
4.166 +
4.167 + for n in numbers:
4.168 + code, response = i.fetch(n, "(RFC822.PEEK)")
4.169 +
4.170 + if code == "OK" and response:
4.171 +
4.172 + # Write the message payload into the cache entry for later
4.173 + # processing.
4.174 +
4.175 + for data in response:
4.176 + try:
4.177 + envelope, body = data
4.178 + message.attach(Parser().parsestr(body))
4.179 + except ValueError:
4.180 + pass
4.181 + else:
4.182 + logging.warning("Could not obtain message %d from folder %s" % (n, folder))
4.183 +
4.184 + cache_entry.write(message.as_string())
4.185 +
4.186 + finally:
4.187 + i.logout()
4.188 + del i
4.189 +
4.190 +def writeCacheHeaders(url, headers, cache_entry):
4.191 +
4.192 + """
4.193 + For the given 'url', write it and the given 'headers' to the given
4.194 + 'cache_entry'.
4.195 + """
4.196 +
4.197 + cache_entry.write(url + "\n")
4.198 + cache_entry.write((headers.get("content-type") or "") + "\n")
4.199 + for key, value in headers.items():
4.200 + if key.lower() != "content-type":
4.201 + cache_entry.write("%s: %s\n" % (key, value))
4.202 + cache_entry.write("\n")
4.203 +
4.204 def getCachedResourceMetadata(f):
4.205
4.206 "Return a metadata dictionary for the given resource file-like object 'f'."
5.1 --- a/MoinSupport.py Wed Jan 28 11:40:31 2015 +0100
5.2 +++ b/MoinSupport.py Wed Jan 28 11:44:22 2015 +0100
5.3 @@ -2,9 +2,9 @@
5.4 """
5.5 MoinMoin - MoinSupport library (derived from EventAggregatorSupport)
5.6
5.7 - @copyright: 2008, 2009, 2010, 2011, 2012, 2013 by Paul Boddie <paul@boddie.org.uk>
5.8 + @copyright: 2008, 2009, 2010, 2011, 2012, 2013, 2014 by Paul Boddie <paul@boddie.org.uk>
5.9 @copyright: 2000-2004 Juergen Hermann <jh@web.de>
5.10 - 2004 by Florian Festi
5.11 + 2004,2006 by Florian Festi
5.12 2006 by Mikko Virkkil
5.13 2005-2008 MoinMoin:ThomasWaldmann
5.14 2007 MoinMoin:ReimarBauer
5.15 @@ -13,11 +13,11 @@
5.16 """
5.17
5.18 from DateSupport import *
5.19 -from ItemSupport import ItemDirectoryStore
5.20 from MoinMoin.parser import text_moin_wiki
5.21 from MoinMoin.Page import Page
5.22 -from MoinMoin.util import lock
5.23 -from MoinMoin import config, search, wikiutil
5.24 +from MoinMoin.support.python_compatibility import hash_new
5.25 +from MoinMoin import caching, config, search, wikiutil
5.26 +from os.path import abspath, exists, join, split
5.27 from shlex import shlex
5.28 import re
5.29 import time
5.30 @@ -36,7 +36,15 @@
5.31 except ImportError:
5.32 pass
5.33
5.34 -__version__ = "0.4"
5.35 +# Static resource location.
5.36 +
5.37 +try:
5.38 + from MoinMoin.web import static
5.39 + htdocs = abspath(join(static.__file__, "htdocs"))
5.40 +except ImportError:
5.41 + htdocs = None
5.42 +
5.43 +__version__ = "0.5"
5.44
5.45 # Extraction of shared fragments.
5.46
5.47 @@ -45,7 +53,8 @@
5.48
5.49 # Extraction of headings.
5.50
5.51 -heading_regexp = re.compile(r"^(?P<level>=+)(?P<heading>.*?)(?P=level)$", re.UNICODE | re.MULTILINE)
5.52 +heading_regexp_str = r"^(?P<level>=+)(?P<heading>.*?)(?P=level)$"
5.53 +heading_regexp = re.compile(heading_regexp_str, re.UNICODE | re.MULTILINE)
5.54
5.55 # Category extraction from pages.
5.56
5.57 @@ -65,6 +74,22 @@
5.58 ur'{{{(?P<preformatted>.*?)}}}'
5.59 ur')', re.UNICODE)
5.60
5.61 +# Access to static Moin content.
5.62 +
5.63 +def getStaticContentDirectory(request):
5.64 +
5.65 + "Use the 'request' to find the htdocs directory."
5.66 +
5.67 + global htdocs
5.68 +
5.69 + if not htdocs:
5.70 + htdocs_in_data = abspath(join(split(request.cfg.data_dir)[0], "htdocs"))
5.71 + if exists(htdocs_in_data):
5.72 + htdocs = htdocs_in_data
5.73 + return htdocs
5.74 +
5.75 + return htdocs
5.76 +
5.77 # Category discovery.
5.78
5.79 def getCategoryPattern(request):
5.80 @@ -191,6 +216,12 @@
5.81 else:
5.82 return None
5.83
5.84 +def groupHasMember(request, groupname, username):
5.85 + if hasattr(request.dicts, "has_member"):
5.86 + return request.dicts.has_member(groupname, username)
5.87 + else:
5.88 + return username in request.groups.get(groupname, [])
5.89 +
5.90 # Searching-related functions.
5.91
5.92 def getPagesFromResults(result_pages, request):
5.93 @@ -805,6 +836,64 @@
5.94
5.95 return {"timestamp" : DateTime(time.gmtime(mtime)[:6] + ("UTC",)), "comment" : comment}
5.96
5.97 +# Page caching functions.
5.98 +
5.99 +def getPageCacheKey(page, request, with_params=False):
5.100 +
5.101 + """
5.102 + Return a cache key for the given 'page' using information in the 'request'.
5.103 + """
5.104 +
5.105 + if hasattr(page, "getCacheKey"):
5.106 + return page.getCacheKey(request, with_params)
5.107 +
5.108 + key = getPageFormatterName(page, request)
5.109 + if request.args and with_params:
5.110 + args = request.args.items()
5.111 + args.sort()
5.112 + key_args = []
5.113 + for k, v in args:
5.114 + key_args.append("%s=%s" % (k, wikiutil.url_quote(v)))
5.115 + arg_str = "&".join(key_args)
5.116 + key = "%s:%s" % (key, hash_new('sha1', arg_str).hexdigest())
5.117 + return key
5.118 +
5.119 +def enforcePageCacheLimit(page, request):
5.120 +
5.121 + """
5.122 + Prevent too many cache entries being stored for the given 'page', using the
5.123 + 'request' to obtain cache items and configuration details.
5.124 + """
5.125 +
5.126 + if hasattr(page, "enforceCacheLimit"):
5.127 + page.enforceCacheLimit(request)
5.128 +
5.129 + keys = caching.get_cache_list(request, page, 'item')
5.130 + try:
5.131 + cache_limit = int(getattr(request.cfg, 'page_cache_limit', "10"))
5.132 + except ValueError:
5.133 + cache_limit = 10
5.134 +
5.135 + if len(keys) >= cache_limit:
5.136 + items = [caching.CacheEntry(request, page, key, scope='item') for key in keys]
5.137 + item_ages = [(item.mtime(), item) for item in items]
5.138 + item_ages.sort()
5.139 + for item_age, item in item_ages[:-cache_limit]:
5.140 + item.remove()
5.141 +
5.142 +def getPageFormatterName(page, request=None):
5.143 +
5.144 + """
5.145 + Return a formatter name as used in the caching system for the given 'page'
5.146 + or using information provided by an optional 'request'.
5.147 + """
5.148 +
5.149 + formatter = getattr(page, 'formatter', None) or request and getattr(request, 'formatter', None)
5.150 + if not formatter:
5.151 + return ''
5.152 + module = formatter.__module__
5.153 + return module[module.rfind('.') + 1:]
5.154 +
5.155 # Page parsing and formatting of embedded content.
5.156
5.157 def getOutputTypes(request, format):
5.158 @@ -911,6 +1000,17 @@
5.159 buf.close()
5.160 return unicode(text, "utf-8")
5.161
5.162 +class RawParser:
5.163 +
5.164 + "A parser that just formats everything as text."
5.165 +
5.166 + def __init__(self, raw, request, **kw):
5.167 + self.raw = raw
5.168 + self.request = request
5.169 +
5.170 + def format(self, fmt, write=None):
5.171 + (write or self.request.write)(fmt.text(self.raw))
5.172 +
5.173 # Finding components for content types.
5.174
5.175 def getParsersForContentType(cfg, mimetype):
5.176 @@ -968,7 +1068,28 @@
5.177
5.178 # NOTE: Re-implementing support for verbatim text and linking avoidance.
5.179
5.180 - return "".join([s for s in verbatim_regexp.split(text) if s is not None])
5.181 + l = []
5.182 + last = 0
5.183 +
5.184 + for m in verbatim_regexp.finditer(text):
5.185 + start, end = m.span()
5.186 + l.append(text[last:start])
5.187 +
5.188 + # Process the verbatim macro arguments.
5.189 +
5.190 + args = m.group("verbatim") or m.group("verbatim2")
5.191 + if args:
5.192 + l += [v for (n, v) in parseMacroArguments(args)]
5.193 +
5.194 + # Or just add the match groups.
5.195 +
5.196 + else:
5.197 + l += [s for s in m.groups() if s]
5.198 +
5.199 + last = end
5.200 +
5.201 + l.append(text[last:])
5.202 + return "".join(l)
5.203
5.204 def getEncodedWikiText(text):
5.205
5.206 @@ -1069,87 +1190,4 @@
5.207 else:
5.208 return title
5.209
5.210 -# Content storage support.
5.211 -
5.212 -class ItemStore(ItemDirectoryStore):
5.213 -
5.214 - "A page-specific item store."
5.215 -
5.216 - def __init__(self, page, item_dir="items", lock_dir="item_locks"):
5.217 -
5.218 - "Initialise an item store for the given 'page'."
5.219 -
5.220 - item_dir_path = tuple(item_dir.split("/"))
5.221 - lock_dir_path = tuple(lock_dir.split("/"))
5.222 - ItemDirectoryStore.__init__(self, page.getPagePath(*item_dir_path), page.getPagePath(*lock_dir_path))
5.223 - self.page = page
5.224 -
5.225 - def can_write(self):
5.226 -
5.227 - """
5.228 - Return whether the user associated with the request can write to the
5.229 - page owning this store.
5.230 - """
5.231 -
5.232 - user = self.page.request.user
5.233 - return user and user.may.write(self.page.page_name)
5.234 -
5.235 - def can_read(self):
5.236 -
5.237 - """
5.238 - Return whether the user associated with the request can read from the
5.239 - page owning this store.
5.240 - """
5.241 -
5.242 - user = self.page.request.user
5.243 - return user and user.may.read(self.page.page_name)
5.244 -
5.245 - def can_delete(self):
5.246 -
5.247 - """
5.248 - Return whether the user associated with the request can delete the
5.249 - page owning this store.
5.250 - """
5.251 -
5.252 - user = self.page.request.user
5.253 - return user and user.may.delete(self.page.page_name)
5.254 -
5.255 - # High-level methods.
5.256 -
5.257 - def append(self, item):
5.258 -
5.259 - "Append the given 'item' to the store."
5.260 -
5.261 - if not self.can_write():
5.262 - return
5.263 -
5.264 - ItemDirectoryStore.append(self, item)
5.265 -
5.266 - def __len__(self):
5.267 -
5.268 - "Return the number of items in the store."
5.269 -
5.270 - if not self.can_read():
5.271 - return 0
5.272 -
5.273 - return ItemDirectoryStore.__len__(self)
5.274 -
5.275 - def __getitem__(self, number):
5.276 -
5.277 - "Return the item with the given 'number'."
5.278 -
5.279 - if not self.can_read():
5.280 - raise IndexError, number
5.281 -
5.282 - return ItemDirectoryStore.__getitem__(self, number)
5.283 -
5.284 - def __delitem__(self, number):
5.285 -
5.286 - "Remove the item with the given 'number'."
5.287 -
5.288 - if not self.can_delete():
5.289 - return
5.290 -
5.291 - return ItemDirectoryStore.__delitem__(self, number)
5.292 -
5.293 # vim: tabstop=4 expandtab shiftwidth=4
6.1 --- a/PKG-INFO Wed Jan 28 11:40:31 2015 +0100
6.2 +++ b/PKG-INFO Wed Jan 28 11:44:22 2015 +0100
6.3 @@ -1,12 +1,12 @@
6.4 Metadata-Version: 1.1
6.5 Name: MoinSupport
6.6 -Version: 0.4.1
6.7 +Version: 0.5
6.8 Author: Paul Boddie
6.9 Author-email: paul at boddie org uk
6.10 Maintainer: Paul Boddie
6.11 Maintainer-email: paul at boddie org uk
6.12 Home-page: http://hgweb.boddie.org.uk/MoinSupport
6.13 -Download-url: http://hgweb.boddie.org.uk/MoinSupport/archive/rel-0-4-1.tar.bz2
6.14 +Download-url: http://hgweb.boddie.org.uk/MoinSupport/archive/rel-0-5.tar.bz2
6.15 Summary: Support libraries for MoinMoin extensions
6.16 License: GPL (version 2 or later)
6.17 Description: The MoinSupport distribution provides libraries handling datetime
7.1 --- a/README.txt Wed Jan 28 11:40:31 2015 +0100
7.2 +++ b/README.txt Wed Jan 28 11:44:22 2015 +0100
7.3 @@ -5,8 +5,7 @@
7.4 extensions. Some of the provided modules can be used independently of
7.5 MoinMoin, such as the ContentTypeSupport, DateSupport, GeneralSupport,
7.6 LocationSupport and ViewSupport modules which do not themselves import any
7.7 -MoinMoin functionality. The ItemSupport module only imports file-locking
7.8 -functionality from MoinMoin and could potentially be used independently.
7.9 +MoinMoin functionality.
7.10
7.11 Installation
7.12 ------------
7.13 @@ -64,6 +63,19 @@
7.14 If time zone handling is not required, pytz need not be installed. It is,
7.15 however, highly recommended that pytz be installed.
7.16
7.17 +New in MoinSupport 0.5 (Changes since MoinSupport 0.4.1)
7.18 +--------------------------------------------------------
7.19 +
7.20 + * Moved ItemStore and related functionality into ItemSupport.
7.21 + * Added support for subpage-based item stores.
7.22 + * Added groupHasMember from ApproveChanges.
7.23 + * Added the TokenSupport module to try and have a reliable shell-like
7.24 + tokeniser.
7.25 + * Added RFC 2822 datetime formatting.
7.26 + * Added a "raw" parser which just formats its input as text.
7.27 + * Added page-related caching functions.
7.28 + * Added access to the static content location of a wiki.
7.29 +
7.30 New in MoinSupport 0.4.1 (Changes since MoinSupport 0.4)
7.31 --------------------------------------------------------
7.32
7.33 @@ -72,6 +84,7 @@
7.34 * Fixed DateSupport to handle NonExistentTimeError.
7.35 * Added macro argument quoting functions.
7.36 * Fixed the quoting of text presented as an argument to the Verbatim macro.
7.37 + * Fixed the extraction of "verbatim" text in getSimpleWikiText.
7.38
7.39 New in MoinSupport 0.4 (Changes since MoinSupport 0.3)
7.40 ------------------------------------------------------
8.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
8.2 +++ b/TokenSupport.py Wed Jan 28 11:44:22 2015 +0100
8.3 @@ -0,0 +1,109 @@
8.4 +# -*- coding: iso-8859-1 -*-
8.5 +"""
8.6 + MoinMoin - TokenSupport library
8.7 +
8.8 + @copyright: 2013 by Paul Boddie <paul@boddie.org.uk>
8.9 + @license: GNU GPL (v2 or later), see COPYING.txt for details.
8.10 +"""
8.11 +
8.12 +import re
8.13 +
8.14 +identifier_expr = re.compile(
8.15 + """(?P<non_literal>[^'" ]+)"""
8.16 + "|"
8.17 + "(?P<spaces> +)"
8.18 + "|"
8.19 + "(?P<literal1>'[^']*')"
8.20 + "|"
8.21 + '(?P<literal2>"[^"]*")'
8.22 + )
8.23 +
8.24 +def getIdentifiers(s, doubling=False):
8.25 +
8.26 + """
8.27 + Return 's' containing space-separated quoted identifiers, parsed into
8.28 + regions that hold the individual identifiers. The optional 'doubling'
8.29 + argument can be used to support convenient quote doubling to reproduce
8.30 + single quote characters.
8.31 +
8.32 + Quoting of identifiers can be done using the single-quote and double-quote
8.33 + characters in order to include spaces within identifiers. For example:
8.34 +
8.35 + 'contains space'
8.36 + -> contains space (a single identifier)
8.37 +
8.38 + Where one kind of quote (or apostrophe) is to be included in an identifier,
8.39 + the other quoting character can be used to delimit the identifier. For
8.40 + example:
8.41 +
8.42 + "Python's syntax"
8.43 + -> Python's syntax (a single identifier)
8.44 +
8.45 + Where the 'doubling' argument is set to a true value, a quote character can
8.46 + be doubled to include it in an identifier. For example:
8.47 +
8.48 + Python''s syntax
8.49 + -> Python's syntax (a single identifier)
8.50 +
8.51 + Where a mixture of quotes is required in a single identifier, adjacent
8.52 + quoted regions can be used. For example:
8.53 +
8.54 + "Python's "'"intuitive" syntax'
8.55 + -> "Python's " (region #1)
8.56 + + '"intuitive" syntax' (region #2)
8.57 + -> Python's "intuitive" syntax (a single identifier)
8.58 +
8.59 + Where unquoted regions are adjacent to quoted regions, the regions are
8.60 + combined. For example:
8.61 +
8.62 + "Python's "intuitive" syntax"
8.63 + -> "Python's " (region #1)
8.64 + + intuitive (region #2)
8.65 + + " syntax" (region #3)
8.66 + -> Python's intuitive syntax (a single identifier)
8.67 + """
8.68 +
8.69 + regions = []
8.70 + in_literal = False
8.71 +
8.72 + for match in identifier_expr.finditer(s):
8.73 + non_literal, spaces, literal1, literal2 = match.groups()
8.74 +
8.75 + identifier = None
8.76 +
8.77 + # Spaces prevent continuation of identifier regions.
8.78 +
8.79 + if spaces:
8.80 + in_literal = False
8.81 +
8.82 + # Unquoted regions contribute to the current identifier.
8.83 +
8.84 + if non_literal and non_literal.strip():
8.85 + identifier = non_literal.strip()
8.86 +
8.87 + # Quoted regions also contribute to the current identifier.
8.88 +
8.89 + for s in (literal1, literal2):
8.90 + if s is not None:
8.91 +
8.92 + # Either strip the quoting or for empty regions, adopt the
8.93 + # quote character.
8.94 +
8.95 + if not doubling or len(s) > 2:
8.96 + identifier = s[1:-1]
8.97 + elif doubling:
8.98 + identifier = s[0]
8.99 +
8.100 + # Either continue or add an identifier, and indicate possible
8.101 + # continuation.
8.102 +
8.103 + if identifier:
8.104 + if in_literal:
8.105 + regions[-1] += identifier
8.106 + else:
8.107 + regions.append(identifier)
8.108 + in_literal = True
8.109 +
8.110 + return regions
8.111 +
8.112 +# vim: tabstop=4 expandtab shiftwidth=4
9.1 --- a/docs/COPYING.txt Wed Jan 28 11:40:31 2015 +0100
9.2 +++ b/docs/COPYING.txt Wed Jan 28 11:44:22 2015 +0100
9.3 @@ -1,14 +1,14 @@
9.4 Licence Agreement
9.5 -----------------
9.6
9.7 -Copyright (C) 2008, 2009, 2010, 2011, 2012, 2013 Paul Boddie <paul@boddie.org.uk>
9.8 +Copyright (C) 2008, 2009, 2010, 2011, 2012, 2013, 2014 Paul Boddie <paul@boddie.org.uk>
9.9
9.10 Some pieces of MoinMoin code were used in this work - typically
9.11 pieces which demonstrate how to perform certain common tasks -
9.12 and are thus covered by the following copyrights:
9.13
9.14 Copyright (C) 2000-2004 Juergen Hermann <jh@web.de>
9.15 -Copyright (C) 2004 by Florian Festi
9.16 +Copyright (C) 2004, 2006 by Florian Festi
9.17 Copyright (C) 2005-2008 MoinMoin:ThomasWaldmann
9.18 Copyright (C) 2006 by Mikko Virkkil
9.19 Copyright (C) 2007 MoinMoin:ReimarBauer
10.1 --- a/setup.py Wed Jan 28 11:40:31 2015 +0100
10.2 +++ b/setup.py Wed Jan 28 11:44:22 2015 +0100
10.3 @@ -8,9 +8,9 @@
10.4 author = "Paul Boddie",
10.5 author_email = "paul@boddie.org.uk",
10.6 url = "http://hgweb.boddie.org.uk/MoinSupport",
10.7 - version = "0.4.1",
10.8 + version = "0.5",
10.9 py_modules = ["ContentTypeSupport", "DateSupport", "GeneralSupport",
10.10 "ItemSupport", "LocationSupport", "MoinDateSupport",
10.11 "MoinRemoteSupport", "MoinSupport", "RecurrenceSupport",
10.12 - "ViewSupport"]
10.13 + "TokenSupport", "ViewSupport"]
10.14 )
11.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
11.2 +++ b/tests/test_tokens.py Wed Jan 28 11:44:22 2015 +0100
11.3 @@ -0,0 +1,19 @@
11.4 +#!/usr/bin/env python
11.5 +
11.6 +from TokenSupport import getIdentifiers
11.7 +
11.8 +tests = [
11.9 + (1, False, """'contains space'""", ["contains space"]),
11.10 + (2, False, """contains space""", ["contains", "space"]),
11.11 + (1, False, '''"Python's syntax"''', ["Python's syntax"]),
11.12 + (2, False, """Python''s syntax""", ["Pythons", "syntax"]),
11.13 + (2, True, """Python''s syntax""", ["Python's", "syntax"]),
11.14 + (1, False, '''"Python's "'"intuitive" syntax' ''', ['''Python's "intuitive" syntax''']),
11.15 + (1, False, '''"Python's "intuitive" syntax" ''', ['''Python's intuitive syntax''']),
11.16 + ]
11.17 +
11.18 +for n, doubling, s, e in tests:
11.19 + l = getIdentifiers(s, doubling)
11.20 + print l == e, l, "==", e, len(l) == n, len(l), "==", n, "<-", doubling, s
11.21 +
11.22 +# vim: tabstop=4 expandtab shiftwidth=4
12.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
12.2 +++ b/tests/test_verbatim.py Wed Jan 28 11:44:22 2015 +0100
12.3 @@ -0,0 +1,23 @@
12.4 +#!/usr/bin/env python
12.5 +
12.6 +from MoinSupport import getSimpleWikiText
12.7 +
12.8 +s = u'''
12.9 +Some normal text.
12.10 +
12.11 +Some !LinkProtected text.
12.12 +
12.13 +Some <<Verbatim(verbatim)>> text.
12.14 +
12.15 +Some <<Verbatim("""verbatim""")>> text.
12.16 +
12.17 +Some <<Verbatim("multi-argument, quoted verbatim")>> text.
12.18 +
12.19 +Some `monospaced` text.
12.20 +
12.21 +Some {{{preformatted}}} text.
12.22 +'''
12.23 +
12.24 +print getSimpleWikiText(s)
12.25 +
12.26 +# vim: tabstop=4 expandtab shiftwidth=4