Merge lp:~mbp/launchpad/flags-webapp into lp:launchpad

Proposed by Martin Pool
Status: Merged
Approved by: Robert Collins
Approved revision: no longer in the source branch.
Merged at revision: 11370
Proposed branch: lp:~mbp/launchpad/flags-webapp
Merge into: lp:launchpad
Diff against target: 683 lines (+428/-60)
10 files modified
lib/canonical/configure.zcml (+2/-1)
lib/canonical/launchpad/webapp/servers.py (+7/-0)
lib/lp/app/browser/tests/base-layout.txt (+2/-1)
lib/lp/app/templates/base-layout.pt (+6/-1)
lib/lp/services/features/__init__.py (+22/-3)
lib/lp/services/features/configure.zcml (+19/-0)
lib/lp/services/features/doc/features.txt (+127/-0)
lib/lp/services/features/flags.py (+117/-38)
lib/lp/services/features/tests/test_flags.py (+85/-16)
lib/lp/services/features/webapp.py (+41/-0)
To merge this branch: bzr merge lp:~mbp/launchpad/flags-webapp
Reviewer Review Type Date Requested Status
Robert Collins (community) Approve
Review via email: mp+32833@code.launchpad.net

Commit message

add flags webapp infrastructure

Description of the change

Since <https://code.edge.launchpad.net/~mbp/launchpad/flags-webapp/+merge/31122> was accepted into db-devel, I'd like to also land it into devel, since the db dependencies are now there.

To post a comment you must log in.
Revision history for this message
Robert Collins (lifeless) wrote :

naturally, please do so.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'lib/canonical/configure.zcml'
--- lib/canonical/configure.zcml 2010-07-24 00:07:54 +0000
+++ lib/canonical/configure.zcml 2010-08-17 02:31:17 +0000
@@ -1,4 +1,4 @@
1<!-- Copyright 2009 Canonical Ltd. This software is licensed under the1<!-- Copyright 2009, 2010 Canonical Ltd. This software is licensed under the
2 GNU Affero General Public License version 3 (see the file LICENSE).2 GNU Affero General Public License version 3 (see the file LICENSE).
3-->3-->
44
@@ -21,6 +21,7 @@
21 <include package="lp.services.job" />21 <include package="lp.services.job" />
22 <include package="lp.services.memcache" />22 <include package="lp.services.memcache" />
23 <include package="lp.services.profile" />23 <include package="lp.services.profile" />
24 <include package="lp.services.features" />
24 <include package="lp.services.scripts" />25 <include package="lp.services.scripts" />
25 <include package="lp.services.worlddata" />26 <include package="lp.services.worlddata" />
26 <include package="lp.services.salesforce" />27 <include package="lp.services.salesforce" />
2728
=== modified file 'lib/canonical/launchpad/webapp/servers.py'
--- lib/canonical/launchpad/webapp/servers.py 2010-08-02 02:23:26 +0000
+++ lib/canonical/launchpad/webapp/servers.py 2010-08-17 02:31:17 +0000
@@ -79,6 +79,10 @@
7979
80from canonical.lazr.timeout import set_default_timeout_function80from canonical.lazr.timeout import set_default_timeout_function
8181
82from lp.services.features.flags import (
83 NullFeatureController,
84 )
85
8286
83class StepsToGo:87class StepsToGo:
84 """88 """
@@ -838,6 +842,9 @@
838 self.needs_datetimepicker_iframe = False842 self.needs_datetimepicker_iframe = False
839 self.needs_json = False843 self.needs_json = False
840 self.needs_gmap2 = False844 self.needs_gmap2 = False
845 # stub out the FeatureController that would normally be provided by
846 # the publication mechanism
847 self.features = NullFeatureController()
841848
842 @property849 @property
843 def uuid(self):850 def uuid(self):
844851
=== modified file 'lib/lp/app/browser/tests/base-layout.txt'
--- lib/lp/app/browser/tests/base-layout.txt 2010-08-04 13:38:22 +0000
+++ lib/lp/app/browser/tests/base-layout.txt 2010-08-17 02:31:17 +0000
@@ -135,10 +135,11 @@
135 Has application tabs: False135 Has application tabs: False
136 Has side portlets: False136 Has side portlets: False
137 At least ... queries issued in ... seconds137 At least ... queries issued in ... seconds
138 Features: {}
139 in scopes {}
138 r...140 r...
139 -->141 -->
140142
141
142Page Headings143Page Headings
143-------------144-------------
144145
145146
=== modified file 'lib/lp/app/templates/base-layout.pt'
--- lib/lp/app/templates/base-layout.pt 2010-08-06 15:40:39 +0000
+++ lib/lp/app/templates/base-layout.pt 2010-08-17 02:31:17 +0000
@@ -14,6 +14,8 @@
14 site_message modules/canonical.config/config/launchpad/site_message;14 site_message modules/canonical.config/config/launchpad/site_message;
15 icingroot string:${rooturl}+icing/rev${revno};15 icingroot string:${rooturl}+icing/rev${revno};
16 icingroot_contrib string:${rooturl}+icing-contrib/rev${revno};16 icingroot_contrib string:${rooturl}+icing-contrib/rev${revno};
17 features request/features;
18 feature_scopes request/features/scopes;
17 CONTEXTS python:{'template':template, 'context': context, 'view':view};19 CONTEXTS python:{'template':template, 'context': context, 'view':view};
18 "20 "
19><metal:doctype define-slot="doctype"><tal:doctype tal:replace="structure string:&lt;!DOCTYPE html PUBLIC &quot;-//W3C//DTD XHTML 1.0 Transitional//EN&quot; &quot;http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd&quot;&gt;" /></metal:doctype>21><metal:doctype define-slot="doctype"><tal:doctype tal:replace="structure string:&lt;!DOCTYPE html PUBLIC &quot;-//W3C//DTD XHTML 1.0 Transitional//EN&quot; &quot;http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd&quot;&gt;" /></metal:doctype>
@@ -171,7 +173,7 @@
171173
172<tal:template>174<tal:template>
173 <tal:comment175 <tal:comment
174 define="log modules/canonical.launchpad.webapp.adapter/summarize_requests"176 define="log modules/canonical.launchpad.webapp.adapter/summarize_requests;"
175 replace="structure string:&lt;!--177 replace="structure string:&lt;!--
176 Facet name: ${view/menu:selectedfacetname}178 Facet name: ${view/menu:selectedfacetname}
177 Page type: ${view/macro:pagetype}179 Page type: ${view/macro:pagetype}
@@ -181,6 +183,9 @@
181183
182 At least ${log}184 At least ${log}
183185
186 Features: ${request/features/usedFlags}
187 in scopes ${request/features/usedScopes}
188
184 r${revno}189 r${revno}
185 --&gt;" />190 --&gt;" />
186</tal:template>191</tal:template>
187192
=== modified file 'lib/lp/services/features/__init__.py'
--- lib/lp/services/features/__init__.py 2010-07-21 11:05:14 +0000
+++ lib/lp/services/features/__init__.py 2010-08-17 02:31:17 +0000
@@ -5,6 +5,25 @@
55
6These can be turned on and off by admins, and can affect particular6These can be turned on and off by admins, and can affect particular
7defined scopes such as "beta users" or "production servers."7defined scopes such as "beta users" or "production servers."
88"""
9See <https://dev.launchpad.net/LEP/FeatureFlags>9
10"""10import threading
11
12
13__all__ = [
14 'getFeatureFlag',
15 'per_thread',
16 ]
17
18
19per_thread = threading.local()
20"""Holds the default per-thread feature controller in its .features attribute.
21
22Framework code is responsible for setting this in the appropriate context, eg
23when starting a web request.
24"""
25
26
27def getFeatureFlag(flag):
28 """Get the value of a flag for this thread's scopes."""
29 return per_thread.features.getFlag(flag)
1130
=== added file 'lib/lp/services/features/configure.zcml'
--- lib/lp/services/features/configure.zcml 1970-01-01 00:00:00 +0000
+++ lib/lp/services/features/configure.zcml 2010-08-17 02:31:17 +0000
@@ -0,0 +1,19 @@
1<!-- Copyright 2010 Canonical Ltd. This software is licensed under the
2 GNU Affero General Public License version 3 (see the file LICENSE).
3-->
4
5<configure
6 xmlns="http://namespaces.zope.org/zope"
7 xmlns:browser="http://namespaces.zope.org/browser">
8
9 <subscriber
10 for="canonical.launchpad.webapp.interfaces.IStartRequestEvent"
11 handler="lp.services.features.webapp.start_request"
12 />
13
14 <subscriber
15 for="zope.app.publication.interfaces.IEndRequestEvent"
16 handler="lp.services.features.webapp.end_request"
17 />
18
19</configure>
020
=== added directory 'lib/lp/services/features/doc'
=== added file 'lib/lp/services/features/doc/features.txt'
--- lib/lp/services/features/doc/features.txt 1970-01-01 00:00:00 +0000
+++ lib/lp/services/features/doc/features.txt 2010-08-17 02:31:17 +0000
@@ -0,0 +1,127 @@
1****************************
2Feature Flag Developer Guide
3****************************
4
5Introduction
6************
7
8The point of feature flags is to let us turn some features of Launchpad on
9and off without changing the code or restarting the application, and to
10expose different features to different subsets of users.
11
12See <https://dev.launchpad.net/LEP/FeatureFlags> for more discussion and
13rationale.
14
15The typical use for feature flags is within web page requests but they can
16also be used in asynchronous jobs or apis or other parts of Launchpad.
17
18Internal model for feature flags
19********************************
20
21A feature flag maps from a *name* to a *value*. The specific value used
22for a particular request is determined by a set of zero or more *scopes*
23that apply to that request, by finding the *rule* with the highest
24*priority*.
25
26Flags are defined by a *name* that typically looks like a Python
27identifier, for example ``notification.global.text``. A definition is
28given for a particular *scope*, which also looks like a dotted identifier,
29for example ``user.beta`` or ``server.edge``. This is just a naming
30convention, and they do not need to correspond to Python modules.
31
32The value is stored in the database as just a Unicode string, and it might
33be interpreted as a boolean, number, human-readable string or whatever.
34
35The default for flags is to be None if they're not set in the database, so
36that should be a sensible baseline default state.
37
38Performance model
39*****************
40
41Flags are supposed to be cheap enough that you can introduce them without
42causing a performance concern.
43
44If the page does not check any flags, no extra work will be done. The
45first time a page checks a flag, all the rules will be read from the
46database and held in memory for the duration of the request.
47
48Scopes may be expensive in some cases, such as checking group membership.
49Whether a scope is active or not is looked up the first time it's needed
50within a particular request.
51
52The standard page footer identifies the flags and scopes that were
53actually used by the page.
54
55Naming conventions
56******************
57
58We have naming conventions for feature flags and scopes, so that people can
59understand the likely impact of a particular flag and so they can find all
60the flags likely to affect a feature.
61
62So for any flag we want to say:
63
64* What application area does this affect? (malone, survey, questions,
65 code, etc)
66
67* What specific feature does it change?
68
69* What affect does it have on this feature? The most common is "enabled"
70 but for some other we want to specify a specific value as well such as
71 "date" or "size".
72
73These are concatenated with dots so the overall feature name looks a bit
74like a Python module name.
75
76A similar approach is used for scopes.
77
78Checking flags in page templates
79********************************
80
81You can conditionally show some text like this::
82
83 <tal:survey condition="features/user_survey.enabled">
84 &nbsp;&bull;&nbsp;
85 <a href="http://survey.example.com/">Take our survey!</a>
86 </tal:survey>
87
88You can use the built-in TAL feature of prepending ``not:`` to the
89condition, and for flags that have a value you could use them in
90``tal:replace`` or ``tal:attributes``.
91
92If you just want to simply insert some text taken from a feature, say
93something like::
94
95 Message of the day: ${motd.text}
96
97Templates can also check whether the request is in a particular scope, but
98before using this consider whether the code will always be bound to that
99scope or whether it would be more correct to define a new feature::
100
101 <p tal:condition="feature_scopes/server.staging">
102 Staging server: all data will be discarded daily!</p>
103
104Checking flags in code
105**********************
106
107The Zope traversal code establishes a `FeatureController` for the duration
108of a request. The object can be obtained through either
109`request.features` or `lp.services.features.per_thread.features`. This
110provides various useful methods including `getFlag` to look up one feature
111(memoized), and `isInScope` to check one scope (also memoized).
112
113As a convenience, `lp.services.features.getFeatureFlag` looks up a single
114flag in the thread default controller.
115
116Debugging feature usage
117***********************
118
119The flags active during a page request, and the scopes that were looked
120up are visible in the comment at the bottom of every standard Launchpad
121page.
122
123Defining scopes
124***************
125
126
127.. vim: ft=rst
0128
=== modified file 'lib/lp/services/features/flags.py'
--- lib/lp/services/features/flags.py 2010-07-22 12:45:51 +0000
+++ lib/lp/services/features/flags.py 2010-08-17 02:31:17 +0000
@@ -1,17 +1,47 @@
1# Copyright 2010 Canonical Ltd. This software is licensed under the1# Copyright 2010 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4__all__ = ['FeatureController']4__all__ = [
5 'FeatureController',
6 'NullFeatureController',
7 ]
8
59
6__metaclass__ = type10__metaclass__ = type
711
812
13from storm.locals import Desc
14
9from lp.services.features.model import (15from lp.services.features.model import (
16 FeatureFlag,
10 getFeatureStore,17 getFeatureStore,
11 FeatureFlag,
12 )18 )
1319
1420
21class Memoize(object):
22
23 def __init__(self, calc):
24 self._known = {}
25 self._calc = calc
26
27 def lookup(self, key):
28 if key in self._known:
29 return self._known[key]
30 v = self._calc(key)
31 self._known[key] = v
32 return v
33
34
35class ScopeDict(object):
36 """Allow scopes to be looked up by getitem"""
37
38 def __init__(self, features):
39 self.features = features
40
41 def __getitem__(self, scope_name):
42 return self.features.isInScope(scope_name)
43
44
15class FeatureController(object):45class FeatureController(object):
16 """A FeatureController tells application code what features are active.46 """A FeatureController tells application code what features are active.
1747
@@ -34,52 +64,101 @@
34 be one per web app request.64 be one per web app request.
3565
36 Intended performance: when this object is first constructed, it will read66 Intended performance: when this object is first constructed, it will read
37 the whole current feature flags from the database. This will take a few67 the whole feature flag table from the database. It is expected to be
38 ms. The controller is then supposed to be held in a thread-local for the68 reasonably small. The scopes may be expensive to compute (eg checking
39 duration of the request.69 team membership) so they are checked at most once when they are first
70 needed.
71
72 The controller is then supposed to be held in a thread-local and reused
73 for the duration of the request.
4074
41 See <https://dev.launchpad.net/LEP/FeatureFlags>75 See <https://dev.launchpad.net/LEP/FeatureFlags>
42 """76 """
4377
44 def __init__(self, scopes):78 def __init__(self, scope_check_callback):
45 """Construct a new view of the features for a set of scopes.79 """Construct a new view of the features for a set of scopes.
46 """80
47 self._store = getFeatureStore()81 :param scope_check_callback: Given a scope name, says whether
48 self._scopes = self._preenScopes(scopes)82 it's active or not.
49 self._cached_flags = self._queryAllFlags()83 """
5084 self._known_scopes = Memoize(scope_check_callback)
51 def getScopes(self):85 self._known_flags = Memoize(self._checkFlag)
52 return frozenset(self._scopes)86 # rules are read from the database the first time they're needed
5387 self._rules = None
54 def getFlag(self, flag_name):88 self.scopes = ScopeDict(self)
55 return self._cached_flags.get(flag_name)89
90 def getFlag(self, flag):
91 """Get the value of a specific flag.
92
93 :param flag: A name to lookup. e.g. 'recipes.enabled'
94 :return: The value of the flag determined by the highest priority rule
95 that matched.
96 """
97 return self._known_flags.lookup(flag)
98
99 def _checkFlag(self, flag):
100 self._needRules()
101 if flag in self._rules:
102 for scope, value in self._rules[flag]:
103 if self._known_scopes.lookup(scope):
104 return value
105
106 def isInScope(self, scope):
107 return self._known_scopes.lookup(scope)
108
109 def __getitem__(self, flag_name):
110 """FeatureController can be indexed.
111
112 This is to support easy zope traversal through eg
113 "request/features/a.b.c". We don't support other collection
114 protocols.
115
116 Note that calling this the first time for any key may cause
117 arbitrarily large amounts of work to be done to determine if the
118 controller is in any scopes relevant to this flag.
119 """
120 return self.getFlag(flag_name)
56121
57 def getAllFlags(self):122 def getAllFlags(self):
58 """Get the feature flags active for the current scopes.123 """Return a dict of all active flags.
59124
60 :returns: dict from flag_name (unicode) to value (unicode).125 This may be expensive because of evaluating many scopes, so it
126 shouldn't normally be used by code that only wants to know about one
127 or a few flags.
61 """128 """
62 return dict(self._cached_flags)129 self._needRules()
130 return dict((f, self.getFlag(f)) for f in self._rules)
63131
64 def _queryAllFlags(self):132 def _loadRules(self):
133 store = getFeatureStore()
65 d = {}134 d = {}
66 rs = (self._store135 rs = (store
67 .find(FeatureFlag,136 .find(FeatureFlag)
68 FeatureFlag.scope.is_in(self._scopes))137 .order_by(Desc(FeatureFlag.priority))
69 .order_by(FeatureFlag.priority)138 .values(FeatureFlag.flag, FeatureFlag.scope,
70 .values(FeatureFlag.flag, FeatureFlag.value))139 FeatureFlag.value))
71 for flag, value in rs:140 for flag, scope, value in rs:
72 d[str(flag)] = value141 d.setdefault(str(flag), []).append((str(scope), value))
73 return d142 return d
74143
75 def _preenScopes(self, scopes):144 def _needRules(self):
76 # for convenience turn strings to unicode145 if self._rules is None:
77 us = []146 self._rules = self._loadRules()
78 for s in scopes:147
79 if isinstance(s, unicode):148 def usedFlags(self):
80 us.append(s)149 """Return dict of flags used in this controller so far."""
81 elif isinstance(s, str):150 return dict(self._known_flags._known)
82 us.append(unicode(s))151
83 else:152 def usedScopes(self):
84 raise TypeError("invalid scope: %r" % s)153 """Return {scope: active} for scopes that have been used so far."""
85 return us154 return dict(self._known_scopes._known)
155
156
157class NullFeatureController(FeatureController):
158 """For use in testing: everything is turned off"""
159
160 def __init__(self):
161 FeatureController.__init__(self, lambda scope: None)
162
163 def _loadRules(self):
164 return []
86165
=== modified file 'lib/lp/services/features/tests/test_flags.py'
--- lib/lp/services/features/tests/test_flags.py 2010-07-22 12:45:51 +0000
+++ lib/lp/services/features/tests/test_flags.py 2010-08-17 02:31:17 +0000
@@ -12,7 +12,11 @@
12from canonical.testing import layers12from canonical.testing import layers
13from lp.testing import TestCase13from lp.testing import TestCase
1414
15from lp.services.features import model15from lp.services.features import (
16 getFeatureFlag,
17 model,
18 per_thread,
19 )
16from lp.services.features.flags import (20from lp.services.features.flags import (
17 FeatureController,21 FeatureController,
18 )22 )
@@ -20,11 +24,10 @@
2024
21notification_name = 'notification.global.text'25notification_name = 'notification.global.text'
22notification_value = u'\N{SNOWMAN} stormy Launchpad weather ahead'26notification_value = u'\N{SNOWMAN} stormy Launchpad weather ahead'
23example_scope = 'beta_user'
2427
2528
26testdata = [29testdata = [
27 (example_scope, notification_name, notification_value, 100),30 ('beta_user', notification_name, notification_value, 100),
28 ('default', 'ui.icing', u'3.0', 100),31 ('default', 'ui.icing', u'3.0', 100),
29 ('beta_user', 'ui.icing', u'4.0', 300),32 ('beta_user', 'ui.icing', u'4.0', 300),
30 ]33 ]
@@ -40,6 +43,14 @@
40 from storm.tracer import debug43 from storm.tracer import debug
41 debug(True)44 debug(True)
4245
46 def makeControllerInScopes(self, scopes):
47 """Make a controller that will report it's in the given scopes."""
48 call_log = []
49 def scope_cb(scope):
50 call_log.append(scope)
51 return scope in scopes
52 return FeatureController(scope_cb), call_log
53
43 def populateStore(self):54 def populateStore(self):
44 store = model.getFeatureStore()55 store = model.getFeatureStore()
45 for (scope, flag, value, priority) in testdata:56 for (scope, flag, value, priority) in testdata:
@@ -49,37 +60,51 @@
49 value=value,60 value=value,
50 priority=priority))61 priority=priority))
5162
52 def test_defaultFlags(self):
53 # the sample db has no flags set
54 control = FeatureController([])
55 self.assertEqual({},
56 control.getAllFlags())
57
58 def test_getFlag(self):63 def test_getFlag(self):
59 self.populateStore()64 self.populateStore()
60 control = FeatureController(['default'])65 control, call_log = self.makeControllerInScopes(['default'])
61 self.assertEqual(u'3.0',66 self.assertEqual(u'3.0',
62 control.getFlag('ui.icing'))67 control.getFlag('ui.icing'))
68 self.assertEqual(['beta_user', 'default'], call_log)
69
70 def test_getItem(self):
71 # for use in page templates, the flags can be treated as a dict
72 self.populateStore()
73 control, call_log = self.makeControllerInScopes(['default'])
74 self.assertEqual(u'3.0',
75 control['ui.icing'])
76 self.assertEqual(['beta_user', 'default'], call_log)
77 # after looking this up the value is known and the scopes are
78 # positively and negatively cached
79 self.assertEqual({'ui.icing': u'3.0'}, control.usedFlags())
80 self.assertEqual(dict(beta_user=False, default=True),
81 control.usedScopes())
6382
64 def test_getAllFlags(self):83 def test_getAllFlags(self):
65 # can fetch all the active flags, and it gives back only the84 # can fetch all the active flags, and it gives back only the
66 # highest-priority settings85 # highest-priority settings. this may be expensive and shouldn't
86 # normally be used.
67 self.populateStore()87 self.populateStore()
68 control = FeatureController(['default', 'beta_user'])88 control, call_log = self.makeControllerInScopes(
89 ['beta_user', 'default'])
69 self.assertEqual(90 self.assertEqual(
70 {'ui.icing': '4.0',91 {'ui.icing': '4.0',
71 notification_name: notification_value},92 notification_name: notification_value},
72 control.getAllFlags())93 control.getAllFlags())
94 # evaluates all necessary flags; in this test data beta_user shadows
95 # default settings
96 self.assertEqual(['beta_user'], call_log)
7397
74 def test_overrideFlag(self):98 def test_overrideFlag(self):
75 # if there are multiple settings for a flag, and they match multiple99 # if there are multiple settings for a flag, and they match multiple
76 # scopes, the priorities determine which is matched100 # scopes, the priorities determine which is matched
77 self.populateStore()101 self.populateStore()
78 default_control = FeatureController(['default'])102 default_control, call_log = self.makeControllerInScopes(['default'])
79 self.assertEqual(103 self.assertEqual(
80 u'3.0',104 u'3.0',
81 default_control.getFlag('ui.icing'))105 default_control.getFlag('ui.icing'))
82 beta_control = FeatureController(['default', 'beta_user'])106 beta_control, call_log = self.makeControllerInScopes(
107 ['beta_user', 'default'])
83 self.assertEqual(108 self.assertEqual(
84 u'4.0',109 u'4.0',
85 beta_control.getFlag('ui.icing'))110 beta_control.getFlag('ui.icing'))
@@ -87,9 +112,53 @@
87 def test_undefinedFlag(self):112 def test_undefinedFlag(self):
88 # if the flag is not defined, we get None113 # if the flag is not defined, we get None
89 self.populateStore()114 self.populateStore()
90 control = FeatureController(['default', 'beta_user'])115 control, call_log = self.makeControllerInScopes(
116 ['beta_user', 'default'])
91 self.assertIs(None,117 self.assertIs(None,
92 control.getFlag('unknown_flag'))118 control.getFlag('unknown_flag'))
93 no_scope_flags = FeatureController([])119 no_scope_flags, call_log = self.makeControllerInScopes([])
94 self.assertIs(None,120 self.assertIs(None,
95 no_scope_flags.getFlag('ui.icing'))121 no_scope_flags.getFlag('ui.icing'))
122
123 def test_threadGetFlag(self):
124 self.populateStore()
125 # the start-of-request handler will do something like this:
126 per_thread.features, call_log = self.makeControllerInScopes(
127 ['default', 'beta_user'])
128 try:
129 # then application code can simply ask without needing a context
130 # object
131 self.assertEqual(u'4.0', getFeatureFlag('ui.icing'))
132 finally:
133 per_thread.features = None
134
135 def testLazyScopeLookup(self):
136 # feature scopes may be a bit expensive to look up, so we do it only
137 # when it will make a difference to the result.
138 self.populateStore()
139 f, call_log = self.makeControllerInScopes(['beta_user'])
140 self.assertEqual(u'4.0', f.getFlag('ui.icing'))
141 # to calculate this it should only have had to check we're in the
142 # beta_users scope; nothing else makes a difference
143 self.assertEqual(dict(beta_user=True), f._known_scopes._known)
144
145 def testUnknownFeature(self):
146 # looking up an unknown feature gives you None
147 self.populateStore()
148 f, call_log = self.makeControllerInScopes([])
149 self.assertEqual(None, f.getFlag('unknown'))
150 # no scopes need to be checked because it's just not in the database
151 # and there's no point checking
152 self.assertEqual({}, f._known_scopes._known)
153 self.assertEquals([], call_log)
154 # however, this we have now negative-cached the flag
155 self.assertEqual(dict(unknown=None), f.usedFlags())
156 self.assertEqual(dict(), f.usedScopes())
157
158 def testScopeDict(self):
159 # can get scopes as a dict, for use by "feature_scopes/server.demo"
160 f, call_log = self.makeControllerInScopes(['beta_user'])
161 self.assertEqual(True, f.scopes['beta_user'])
162 self.assertEqual(False, f.scopes['alpha_user'])
163 self.assertEqual(True, f.scopes['beta_user'])
164 self.assertEqual(['beta_user', 'alpha_user'], call_log)
96165
=== added file 'lib/lp/services/features/webapp.py'
--- lib/lp/services/features/webapp.py 1970-01-01 00:00:00 +0000
+++ lib/lp/services/features/webapp.py 2010-08-17 02:31:17 +0000
@@ -0,0 +1,41 @@
1# Copyright 2010 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Connect Feature flags into webapp requests."""
5
6__all__ = []
7
8__metaclass__ = type
9
10import canonical.config
11
12from lp.services.features import (
13 per_thread,
14 )
15from lp.services.features.flags import (
16 FeatureController,
17 )
18
19
20class ScopesFromRequest(object):
21 """Identify feature scopes based on request state."""
22
23 def __init__(self, request):
24 self._request = request
25
26 def lookup(self, scope_name):
27 parts = scope_name.split('.')
28 if len(parts) == 2:
29 if parts[0] == 'server':
30 return canonical.config.config['launchpad']['is_' + parts[1]]
31
32
33def start_request(event):
34 """Register FeatureController."""
35 event.request.features = per_thread.features = FeatureController(
36 ScopesFromRequest(event.request).lookup)
37
38
39def end_request(event):
40 """Done with this FeatureController."""
41 event.request.features = per_thread.features = None