Skip to content

Commit 7cbc2e3

Browse files
author
Sam Ruby
committed
Theme support
1 parent 9fa9fb6 commit 7cbc2e3

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+1464
-62
lines changed

planet/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22

33
logger = None
44

5+
import config
6+
config.__init__()
7+
58
def getLogger(level):
69
""" get a logger with the specified log level """
710
global logger

planet/config.py

Lines changed: 48 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
config.load('config.ini')
1212
1313
# administrative / structural information
14-
print config.templates()
14+
print config.template_files()
1515
print config.feeds()
1616
1717
# planet wide configuration
@@ -37,6 +37,7 @@ def __init__():
3737
"""define the struture of an ini file"""
3838
import config
3939

40+
# underlying implementation
4041
def get(section, option, default):
4142
if section and parser.has_option(section, option):
4243
return parser.get(section, option)
@@ -49,6 +50,10 @@ def define_planet(name, default):
4950
setattr(config, name, lambda default=default: get(None,name,default))
5051
planet_predefined_options.append(name)
5152

53+
def define_planet_list(name):
54+
setattr(config, name, lambda : get(None,name,'').split())
55+
planet_predefined_options.append(name)
56+
5257
def define_tmpl(name, default):
5358
setattr(config, name, lambda section, default=default:
5459
get(section,name,default))
@@ -63,25 +68,57 @@ def define_tmpl_int(name, default):
6368
define_planet('cache_directory', "cache")
6469
define_planet('log_level', "WARNING")
6570
define_planet('feed_timeout', 20)
71+
define_planet('date_format', "%B %d, %Y %I:%M %p")
72+
define_planet('generator', 'Venus')
73+
define_planet('generator_uri', 'http://intertwingly.net/code/venus/')
74+
define_planet('owner_name', 'Anonymous Coward')
75+
define_planet('owner_email', '')
76+
define_planet('output_theme', '')
77+
define_planet('output_dir', 'output')
78+
79+
define_planet_list('template_files')
80+
define_planet_list('bill_of_materials')
81+
define_planet_list('template_directories')
6682

6783
# template options
6884
define_tmpl_int('days_per_page', 0)
6985
define_tmpl_int('items_per_page', 60)
7086
define_tmpl('encoding', 'utf-8')
7187

72-
# prevent re-initialization
73-
setattr(config, '__init__', lambda: None)
74-
75-
def load(file):
88+
def load(config_file):
7689
""" initialize and load a configuration"""
77-
__init__()
7890
global parser
7991
parser = ConfigParser()
80-
parser.read(file)
81-
82-
def template_files():
83-
""" list the templates defined """
84-
return parser.get('Planet','template_files').split(' ')
92+
parser.read(config_file)
93+
94+
if parser.has_option('Planet', 'output_theme'):
95+
theme = parser.get('Planet', 'output_theme')
96+
for path in ("", os.path.join(sys.path[0],'themes')):
97+
theme_dir = os.path.join(path,theme)
98+
theme_file = os.path.join(theme_dir,'config.ini')
99+
if os.path.exists(theme_file):
100+
# initial search list for theme directories
101+
dirs = [theme_dir]
102+
if parser.has_option('Planet', 'template_directories'):
103+
dirs.insert(0,parser.get('Planet', 'template_directories'))
104+
105+
# read in the theme
106+
parser = ConfigParser()
107+
parser.read(theme_file)
108+
109+
# complete search list for theme directories
110+
if parser.has_option('Planet', 'template_directories'):
111+
dirs += [os.path.join(theme_dir,dir) for dir in
112+
parser.get('Planet', 'template_directories').split()]
113+
114+
# merge configurations, allowing current one to override theme
115+
parser.read(config_file)
116+
parser.set('Planet', 'template_directories', ' '.join(dirs))
117+
break
118+
else:
119+
import config, planet
120+
log = planet.getLogger(config.log_level())
121+
log.error('Unable to find theme %s', theme)
85122

86123
def cache_sources_directory():
87124
if parser.has_option('Planet', 'cache_sources_directory'):

planet/reconstitute.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
from xml.dom import minidom
1919
from BeautifulSoup import BeautifulSoup
2020
from xml.parsers.expat import ExpatError
21-
import planet
21+
import planet, config
2222

2323
illegal_xml_chars = re.compile("[\x01-\x08\x0B\x0C\x0E-\x1F]")
2424

@@ -29,6 +29,7 @@ def createTextElement(parent, name, value):
2929
xelement = xdoc.createElement(name)
3030
xelement.appendChild(xdoc.createTextNode(value))
3131
parent.appendChild(xelement)
32+
return xelement
3233

3334
def invalidate(c):
3435
""" replace invalid characters """
@@ -98,7 +99,9 @@ def date(xentry, name, parsed):
9899
""" insert a date-formated element into the entry """
99100
if not parsed: return
100101
formatted = time.strftime("%Y-%m-%dT%H:%M:%SZ", parsed)
101-
createTextElement(xentry, name, formatted)
102+
xdate = createTextElement(xentry, name, formatted)
103+
formatted = time.strftime(config.date_format(), parsed)
104+
xdate.setAttribute('planet:format', formatted)
102105

103106
def author(xentry, name, detail):
104107
""" insert an author-like element into the entry """

planet/splice.py

Lines changed: 83 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
""" Splice together a planet from a cache of feed entries """
2-
import glob, os
2+
import glob, os, time, shutil
33
from xml.dom import minidom
44
import planet, config, feedparser, reconstitute
5-
from reconstitute import createTextElement
5+
from reconstitute import createTextElement, date
66
from spider import filename
77

88
def splice(configFile):
@@ -11,24 +11,28 @@ def splice(configFile):
1111
config.load(configFile)
1212
log = planet.getLogger(config.log_level())
1313

14+
log.info("Loading cached data")
1415
cache = config.cache_directory()
1516
dir=[(os.stat(file).st_mtime,file) for file in glob.glob(cache+"/*")
1617
if not os.path.isdir(file)]
1718
dir.sort()
1819
dir.reverse()
1920

2021
items=max([config.items_per_page(templ)
21-
for templ in config.template_files()])
22+
for templ in config.template_files() or ['Planet']])
2223

2324
doc = minidom.parseString('<feed xmlns="http://www.w3.org/2005/Atom"/>')
2425
feed = doc.documentElement
2526

26-
# insert Google/LiveJournal's noindex
27-
feed.setAttribute('indexing:index','no')
28-
feed.setAttribute('xmlns:indexing','urn:atom-extension:indexing')
29-
3027
# insert feed information
3128
createTextElement(feed, 'title', config.name())
29+
date(feed, 'updated', time.gmtime())
30+
gen = createTextElement(feed, 'generator', config.generator())
31+
gen.setAttribute('uri', config.generator_uri())
32+
author = doc.createElement('author')
33+
createTextElement(author, 'name', config.owner_name())
34+
createTextElement(author, 'email', config.owner_email())
35+
feed.appendChild(author)
3236

3337
# insert entry information
3438
for mtime,file in dir[:items]:
@@ -47,3 +51,75 @@ def splice(configFile):
4751
feed.appendChild(xdoc.documentElement)
4852

4953
return doc
54+
55+
def apply(doc):
56+
output_dir = config.output_dir()
57+
if not os.path.exists(output_dir): os.makedirs(output_dir)
58+
log = planet.getLogger(config.log_level())
59+
60+
try:
61+
# if available, use the python interface to libxslt
62+
import libxml2
63+
import libxslt
64+
dom = libxml2.parseDoc(doc)
65+
docfile = None
66+
except:
67+
# otherwise, use the command line interface
68+
dom = None
69+
import warnings
70+
warnings.simplefilter('ignore', RuntimeWarning)
71+
docfile = os.tmpnam()
72+
file = open(docfile,'w')
73+
file.write(doc)
74+
file.close()
75+
76+
# Go-go-gadget-template
77+
for template_file in config.template_files():
78+
for template_dir in config.template_directories():
79+
template_resolved = os.path.join(template_dir, template_file)
80+
if os.path.exists(template_resolved): break
81+
else:
82+
log.error("Unable to locate template %s", template_file)
83+
continue
84+
85+
base,ext = os.path.splitext(os.path.basename(template_resolved))
86+
if ext != '.xslt':
87+
log.warning("Skipping template %s", template_resolved)
88+
continue
89+
90+
log.info("Processing template %s", template_resolved)
91+
output_file = os.path.join(output_dir, base)
92+
if dom:
93+
styledoc = libxml2.parseFile(template_resolved)
94+
style = libxslt.parseStylesheetDoc(styledoc)
95+
result = style.applyStylesheet(dom, None)
96+
log.info("Writing %s", output_file)
97+
style.saveResultToFilename(output_file, result, 0)
98+
style.freeStylesheet()
99+
result.freeDoc()
100+
else:
101+
log.info("Writing %s", output_file)
102+
os.system('xsltproc %s %s > %s' %
103+
(template_resolved, docfile, output_file))
104+
105+
if dom: dom.freeDoc()
106+
if docfile: os.unlink(docfile)
107+
108+
# Process bill of materials
109+
for copy_file in config.bill_of_materials():
110+
dest = os.path.join(output_dir, copy_file)
111+
for template_dir in config.template_directories():
112+
source = os.path.join(template_dir, copy_file)
113+
if os.path.exists(source): break
114+
else:
115+
log.error('Unable to locate %s', copy_file)
116+
continue
117+
118+
mtime = os.stat(source).st_mtime
119+
if not os.path.exists(dest) or os.stat(dest).st_mtime < mtime:
120+
dest_dir = os.path.split(dest)[0]
121+
if not os.path.exists(dest_dir): os.makedirs(dest_dir)
122+
123+
log.info("Copying %s to %s", source, dest)
124+
shutil.copyfile(source, dest)
125+
shutil.copystat(source, dest)

runtests.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
#!/usr/bin/env python
2-
import glob, trace, unittest
2+
import glob, trace, unittest, os, sys
3+
4+
# start in a consistent, predictable location
5+
os.chdir(sys.path[0])
36

47
# find all of the planet test modules
5-
modules = map(trace.fullmodname, glob.glob('tests/test_*.py'))
8+
modules = map(trace.fullmodname, glob.glob(os.path.join('tests', 'test_*.py')))
69

710
# load all of the tests into a suite
811
suite = unittest.TestLoader().loadTestsFromNames(modules)

splice.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,7 @@
1313
# at the moment, we don't have template support, so we cheat and
1414
# simply insert a XSLT processing instruction
1515
doc = splice.splice(sys.argv[1])
16-
pi = doc.createProcessingInstruction(
17-
'xml-stylesheet','type="text/xsl" href="planet.xslt"')
18-
doc.insertBefore(pi, doc.firstChild)
19-
print doc.toxml('utf-8')
16+
splice.apply(doc.toxml('utf-8'))
2017
else:
2118
print "Usage:"
2219
print " python %s config.ini" % sys.argv[0]

tests/capture.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
#!/usr/bin/env python
2+
3+
"""
4+
While unit tests are intended to be independently executable, it often
5+
is helpful to ensure that some downstream tasks can be run with the
6+
exact output produced by upstream tasks.
7+
8+
This script captures such output. It should be run whenever there is
9+
a major change in the contract between stages
10+
"""
11+
12+
import shutil, os, sys
13+
14+
# move up a directory
15+
sys.path.insert(1, os.path.split(sys.path[0])[0])
16+
os.chdir(sys.path[1])
17+
18+
# copy spider output to splice input
19+
from planet import spider
20+
spider.spiderPlanet('tests/data/spider/config.ini')
21+
if os.path.exists('tests/data/splice/cache'):
22+
shutil.rmtree('tests/data/splice/cache')
23+
shutil.move('tests/work/spider/cache', 'tests/data/splice/cache')
24+
25+
source=open('tests/data/spider/config.ini')
26+
dest1=open('tests/data/splice/config.ini', 'w')
27+
dest1.write(source.read().replace('/work/spider/', '/data/splice/'))
28+
dest1.close()
29+
30+
source.seek(0)
31+
dest2=open('tests/data/apply/config.ini', 'w')
32+
dest2.write(source.read().replace('[Planet]', '''[Planet]
33+
output_theme = asf
34+
output_dir = tests/work/apply'''))
35+
dest2.close()
36+
source.close()
37+
38+
# copy splice output to apply input
39+
from planet import splice
40+
file=open('tests/data/apply/feed.xml', 'w')
41+
file.write(splice.splice('tests/data/splice/config.ini').toxml('utf-8'))
42+
file.close()

tests/data/apply/config.ini

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
[Planet]
2+
output_theme = asf
3+
output_dir = tests/work/apply
4+
name = test planet
5+
cache_directory = tests/work/spider/cache
6+
7+
[tests/data/spider/testfeed0.atom]
8+
name = not found
9+
10+
[tests/data/spider/testfeed1b.atom]
11+
name = one
12+
13+
[tests/data/spider/testfeed2.atom]
14+
name = two
15+
16+
[tests/data/spider/testfeed3.rss]
17+
name = three

0 commit comments

Comments
 (0)