diff options
author | Mike Crute <mcrute@gmail.com> | 2015-07-28 21:15:06 -0700 |
---|---|---|
committer | Mike Crute <mcrute@gmail.com> | 2015-07-28 21:15:06 -0700 |
commit | 6da717bf333dc82c67d7aece3fd36f97090040f8 (patch) | |
tree | 056bb5e2ef5642d836425c2ac85212d3328364bb | |
download | foundry-master.tar.bz2 foundry-master.tar.xz foundry-master.zip |
-rw-r--r-- | foundry/__init__.py | 11 | ||||
-rw-r--r-- | foundry/application.py | 63 | ||||
-rw-r--r-- | foundry/controllers/__init__.py | 33 | ||||
-rw-r--r-- | foundry/di.py | 16 | ||||
-rw-r--r-- | foundry/interfaces.py | 23 | ||||
-rw-r--r-- | foundry/router.py | 22 | ||||
-rw-r--r-- | foundry/template_filters.py | 53 | ||||
-rw-r--r-- | foundry/test_router.py | 159 | ||||
-rw-r--r-- | foundry/utils.py | 106 | ||||
-rw-r--r-- | foundry/vcs/__init__.py | 0 | ||||
-rw-r--r-- | foundry/vcs/hg/__init__.py | 0 | ||||
-rw-r--r-- | foundry/vcs/hg/providers.py | 24 | ||||
-rw-r--r-- | foundry/views/__init__.py | 23 | ||||
-rw-r--r-- | foundry/views/changelog.tpl | 42 | ||||
-rwxr-xr-x | setup.py | 18 | ||||
-rw-r--r-- | tests/test_frozendict.py | 19 | ||||
-rw-r--r-- | tests/test_implements.py | 51 |
17 files changed, 663 insertions, 0 deletions
diff --git a/foundry/__init__.py b/foundry/__init__.py new file mode 100644 index 0000000..c71d252 --- /dev/null +++ b/foundry/__init__.py | |||
@@ -0,0 +1,11 @@ | |||
1 | # vim: set filencoding=utf8 | ||
2 | """ | ||
3 | Foundary | ||
4 | |||
5 | @author: Mike Crute (mcrute@gmail.com) | ||
6 | @organization: SoftGroup Interactive, Inc. | ||
7 | @date: May 02, 2010 | ||
8 | """ | ||
9 | |||
10 | |||
11 | __version__ = "0.1" | ||
diff --git a/foundry/application.py b/foundry/application.py new file mode 100644 index 0000000..bc4a569 --- /dev/null +++ b/foundry/application.py | |||
@@ -0,0 +1,63 @@ | |||
1 | #!/usr/bin/env python | ||
2 | # vim: set filencoding=utf8 | ||
3 | """ | ||
4 | Foundry Main Application | ||
5 | |||
6 | @author: Mike Crute (mcrute@gmail.com) | ||
7 | @organization: SoftGroup Interactive, Inc. | ||
8 | @date: May 02, 2010 | ||
9 | |||
10 | Entry point for the application. All of the main app | ||
11 | wireup happens here. This is also where you can get | ||
12 | the WSGI application object. This isn't strictly | ||
13 | required. You could wire this all up by hand. | ||
14 | """ | ||
15 | |||
16 | import jinja2 | ||
17 | from snakeguice import Injector | ||
18 | from snakeguice.extras.snakeweb import Application, AutoRoutesModule | ||
19 | |||
20 | from foundry.utils import frozendict | ||
21 | from foundry import controllers, interfaces | ||
22 | from foundry.views import JinjaRenderer | ||
23 | from foundry.template_filters import TEMPLATE_FILTERS | ||
24 | |||
25 | from foundry.vcs.hg.providers import RepoProvider | ||
26 | |||
27 | |||
28 | class MainModule(object): | ||
29 | |||
30 | def configure(self, binder): | ||
31 | #------------------------------------------------------------- | ||
32 | # Mercurial Bindings | ||
33 | #------------------------------------------------------------- | ||
34 | binder.bind(interfaces.RepositoryProvider, to=RepoProvider) | ||
35 | |||
36 | #------------------------------------------------------------- | ||
37 | # Template Engine Bindings | ||
38 | #------------------------------------------------------------- | ||
39 | loader = jinja2.PackageLoader('foundry', 'views') | ||
40 | tpl_env = jinja2.Environment(loader=loader) | ||
41 | tpl_env.filters.update(TEMPLATE_FILTERS) | ||
42 | renderer = JinjaRenderer(tpl_env) | ||
43 | |||
44 | binder.bind(interfaces.TemplateRenderer, to_instance=renderer) | ||
45 | |||
46 | |||
47 | class MapperModule(AutoRoutesModule): | ||
48 | |||
49 | configured_routes = frozendict({ | ||
50 | '/': controllers.ChangelogController, | ||
51 | }) | ||
52 | |||
53 | |||
54 | def get_application(): | ||
55 | injector = Injector([MainModule(), MapperModule()]) | ||
56 | return Application(injector) | ||
57 | |||
58 | |||
59 | if __name__ == '__main__': | ||
60 | from wsgiref.simple_server import make_server | ||
61 | |||
62 | httpd = make_server('', 8080, get_application()) | ||
63 | httpd.serve_forever() | ||
diff --git a/foundry/controllers/__init__.py b/foundry/controllers/__init__.py new file mode 100644 index 0000000..b17b3e9 --- /dev/null +++ b/foundry/controllers/__init__.py | |||
@@ -0,0 +1,33 @@ | |||
1 | from webob import Response | ||
2 | from datetime import datetime | ||
3 | from foundry import interfaces | ||
4 | from foundry.di import inject, Injected | ||
5 | |||
6 | |||
7 | class Changeset(object): | ||
8 | |||
9 | def __init__(self, changeset): | ||
10 | self.changeset = changeset | ||
11 | |||
12 | def __getattr__(self, key): | ||
13 | return getattr(self.changeset, key) | ||
14 | |||
15 | def date(self): | ||
16 | return datetime.fromtimestamp(float(self.changeset.date()[0])) | ||
17 | |||
18 | |||
19 | class ChangelogController(object): | ||
20 | |||
21 | @inject(repo=interfaces.RepositoryProvider, | ||
22 | renderer=interfaces.TemplateRenderer) | ||
23 | def __init__(self, repo=Injected, renderer=Injected): | ||
24 | self.repo = repo.get('/Users/mcrute') | ||
25 | self.renderer = renderer | ||
26 | |||
27 | def __call__(self, request): | ||
28 | def _repo_iter(): | ||
29 | for rev in self.repo: | ||
30 | yield Changeset(self.repo[rev]) | ||
31 | |||
32 | return Response(self.renderer.render('changelog.tpl', | ||
33 | repo=_repo_iter())) | ||
diff --git a/foundry/di.py b/foundry/di.py new file mode 100644 index 0000000..d283871 --- /dev/null +++ b/foundry/di.py | |||
@@ -0,0 +1,16 @@ | |||
1 | # vim: set filencoding=utf8 | ||
2 | """ | ||
3 | Dependency Injection Utils | ||
4 | |||
5 | @author: Mike Crute (mcrute@gmail.com) | ||
6 | @organization: SoftGroup Interactive, Inc. | ||
7 | @date: May 02, 2010 | ||
8 | |||
9 | This module provides a convenient place to replace the | ||
10 | core DI functions in the case that somebody would want | ||
11 | to manually assemble the application without the help | ||
12 | of a DI system. | ||
13 | """ | ||
14 | |||
15 | |||
16 | from snakeguice import inject, Injected | ||
diff --git a/foundry/interfaces.py b/foundry/interfaces.py new file mode 100644 index 0000000..4717778 --- /dev/null +++ b/foundry/interfaces.py | |||
@@ -0,0 +1,23 @@ | |||
1 | # vim: set filencoding=utf8 | ||
2 | """ | ||
3 | Foundry Interfaces | ||
4 | |||
5 | @author: Mike Crute (mcrute@gmail.com) | ||
6 | @organization: SoftGroup Interactive, Inc. | ||
7 | @date: May 02, 2010 | ||
8 | """ | ||
9 | |||
10 | |||
11 | class RepositoryProvider: | ||
12 | """ | ||
13 | Repository providers return an instance of a repository. | ||
14 | """ | ||
15 | |||
16 | def get(self, repo_path=''): | ||
17 | pass | ||
18 | |||
19 | |||
20 | class TemplateRenderer: | ||
21 | |||
22 | def render(self, template, *args, **kwargs): | ||
23 | pass | ||
diff --git a/foundry/router.py b/foundry/router.py new file mode 100644 index 0000000..233fa30 --- /dev/null +++ b/foundry/router.py | |||
@@ -0,0 +1,22 @@ | |||
1 | # vim: set filencoding=utf8 | ||
2 | """ | ||
3 | RESTful URL Router | ||
4 | |||
5 | @author: Mike Crute (mcrute@gmail.com) | ||
6 | @organization: SoftGroup Interactive, Inc. | ||
7 | @date: May 20, 2010 | ||
8 | """ | ||
9 | |||
10 | |||
11 | class Renderer(object): | ||
12 | pass | ||
13 | |||
14 | |||
15 | class JSONRenderer(Renderer): | ||
16 | |||
17 | can_handle = ('application/json', 'text/json') | ||
18 | |||
19 | |||
20 | |||
21 | class Resource(object): | ||
22 | pass | ||
diff --git a/foundry/template_filters.py b/foundry/template_filters.py new file mode 100644 index 0000000..6dec2e7 --- /dev/null +++ b/foundry/template_filters.py | |||
@@ -0,0 +1,53 @@ | |||
1 | # vim: set filencoding=utf8 | ||
2 | """ | ||
3 | Foundry Template Filters | ||
4 | |||
5 | @author: Mike Crute (mcrute@gmail.com) | ||
6 | @organization: SoftGroup Interactive, Inc. | ||
7 | @date: May 03, 2010 | ||
8 | """ | ||
9 | |||
10 | |||
11 | from datetime import datetime | ||
12 | from foundry.utils import frozendict | ||
13 | |||
14 | |||
15 | def pluralize(word, how_many=1): | ||
16 | "Naive pluralization function." | ||
17 | return word if how_many == 1 else word + "s" | ||
18 | |||
19 | |||
20 | def nice_date_delta(from_date, to_date=datetime.now()): | ||
21 | """ | ||
22 | Provides a friendly text representation (ie. 7 months) | ||
23 | for the delta between two dates. | ||
24 | """ | ||
25 | delta = to_date - from_date | ||
26 | |||
27 | months = delta.days / 30 | ||
28 | if months > 0: | ||
29 | return "{0} {1}".format(months, pluralize("month", months)) | ||
30 | |||
31 | weeks = delta.days / 7 | ||
32 | if weeks > 0: | ||
33 | return "{0} {1}".format(weeks, pluralize("week", weeks)) | ||
34 | |||
35 | if delta.days > 0: | ||
36 | return "{0} {1}".format(delta.days, pluralize("day", delta.days)) | ||
37 | |||
38 | hours = delta.seconds / (60 * 60) | ||
39 | if hours > 0: | ||
40 | return "{0} {1}".format(hours, pluralize("hour", hours)) | ||
41 | |||
42 | minutes = delta.seconds / 60 | ||
43 | if minutes > 0: | ||
44 | return "{0} {1}".format(minutes, pluralize("minute", minutes)) | ||
45 | |||
46 | return "seconds ago" | ||
47 | |||
48 | |||
49 | #: Template filter registry | ||
50 | TEMPLATE_FILTERS = frozendict({ | ||
51 | 'nice_date_delta': nice_date_delta, | ||
52 | 'pluralize': pluralize, | ||
53 | }) | ||
diff --git a/foundry/test_router.py b/foundry/test_router.py new file mode 100644 index 0000000..15e97fd --- /dev/null +++ b/foundry/test_router.py | |||
@@ -0,0 +1,159 @@ | |||
1 | from router import Router, Resource | ||
2 | from router import JSONRenderer, HTMLRenderer, XMLRenderer, AtomRenderer | ||
3 | |||
4 | |||
5 | router = Router() | ||
6 | |||
7 | # Add Renderers | ||
8 | router.add_renderer(JSONRenderer, default=True) | ||
9 | router.add_renderer(HTMLRenderer) | ||
10 | router.add_renderer(AtomRenderer) | ||
11 | |||
12 | router.add_auth_source(DbAuthenticator('users.db')) | ||
13 | router.add_authenz_source(DbAuthorizor('users.db')) | ||
14 | |||
15 | revision_spec = FragmentSpec(required=False, default='tip', | ||
16 | regex='(tip|[0-9a-zA-Z]+)') | ||
17 | |||
18 | # Add Resource Mappings | ||
19 | Route('/{project_name}', ProjectSummaryResource, [ | ||
20 | Route('/summary', ProjectSummaryResource), | ||
21 | Route('/shortlog/{revision}', ShortLogResource, | ||
22 | uri_spec={ 'revision': revision_spec }), | ||
23 | Route('/graph/{revision}', GraphResource, | ||
24 | uri_spec={ 'revision': revision_spec }), | ||
25 | Route('/raw-rev/{revision}', RawRevisionResource, | ||
26 | uri_spec={ 'revision': revision_spec }), | ||
27 | Route('/tags', TagsResource), | ||
28 | Route('/annotate/{revision}/{filename}', AnnotateResource, | ||
29 | uri_spec={ 'revision': revision_spec, | ||
30 | 'filename': FragmentSpec(required=False) }), | ||
31 | Route(('/diff/{revision}/{filename}', '/filediff/{revision}/{filename}'), | ||
32 | DiffResource, | ||
33 | uri_spec={ 'revision': revision_spec, | ||
34 | 'filename': FragmentSpec(required=False) }), | ||
35 | Route('/raw-file/{revision}/{filename}', RawFileResource, | ||
36 | uri_spec={ 'revision': revision_spec, | ||
37 | 'filename': FragmentSpec(required=False) }), | ||
38 | Route('/branches', BranchesResource), | ||
39 | Route('/archive/{revision}.tar.{format}', ArchiveResource, | ||
40 | uri_spec={ 'revision': FragmentSpec(regex='(tip|[0-9a-zA-Z]+)'), | ||
41 | 'format': FragmentSpec(value_list=('gz', 'bz2') }), | ||
42 | Route(('/rev/{revision}','/changeset/{revision}'), RevisionResource, | ||
43 | uri_spec={ 'revision': revision_spec }), | ||
44 | Route(('/log/{revision}', '/changelog/{revision}', '/filelog/{revision}'), | ||
45 | LogResource, | ||
46 | uri_spec={ 'revision': revision_spec }), | ||
47 | Route('/file/{revision}/{filename}', FileResource, | ||
48 | uri_spec={ 'revision': revision_spec, | ||
49 | 'filename': FragmentSpec(required=False) }), | ||
50 | ]) | ||
51 | |||
52 | |||
53 | class constant(object): | ||
54 | """ | ||
55 | Constant descriptor to provide a level of protection against | ||
56 | changes to constants in class instances. Does not prevent | ||
57 | changes directly to constants in non-instances. | ||
58 | """ | ||
59 | |||
60 | def __init__(self, value): | ||
61 | self.value = value | ||
62 | |||
63 | def __get__(self, instance, owner): | ||
64 | return self.value | ||
65 | |||
66 | def __set__(self, instance, value): | ||
67 | raise ValueError('Can not assign to constant.') | ||
68 | |||
69 | def __delete__(self, instance): | ||
70 | raise ValueError('Can not delete constant.') | ||
71 | |||
72 | |||
73 | class Route(object): | ||
74 | |||
75 | # ---------------------------------------------------------------- | ||
76 | # Constants | ||
77 | # ---------------------------------------------------------------- | ||
78 | REDIRECT = constant('redirect') | ||
79 | FAILURE = constant('failure') | ||
80 | |||
81 | # ---------------------------------------------------------------- | ||
82 | # Required Parameters | ||
83 | # ---------------------------------------------------------------- | ||
84 | |||
85 | #: Part of the URI that will map to this route | ||
86 | uri_part = None | ||
87 | |||
88 | #: Resource existing at this route | ||
89 | resource = None | ||
90 | |||
91 | #: Resource requires that the user is using an HTTPS connection | ||
92 | #: can be True, False, REDIRECT or FAILRE. If True and no HTTPS | ||
93 | #: the route will fail to match. If REDIRECT and no HTTPS the | ||
94 | #: client will be redirect to the secure version of the resouce. | ||
95 | #: If FAILURE a 403 (Forbidden) error will be returned to the | ||
96 | #: client. | ||
97 | https = False | ||
98 | |||
99 | #: Route requires authentication | ||
100 | requires_authentication = True | ||
101 | |||
102 | # ---------------------------------------------------------------- | ||
103 | # Optional, if unspecified these will not be used | ||
104 | # ---------------------------------------------------------------- | ||
105 | |||
106 | #: Dictionary of uri fragment names to FragmentSpec objects used | ||
107 | #: to validate the individual uri fragments | ||
108 | uri_spec = None | ||
109 | |||
110 | #: Callable or list of callables to which the matching resouce | ||
111 | #: and context will be passed, the first failure will cause the | ||
112 | #: route to not match. | ||
113 | conditions = None | ||
114 | |||
115 | #: Type of permission required. The router doesn't care what | ||
116 | #: this object is as long as the security system understands it | ||
117 | permissions_required = None | ||
118 | |||
119 | # ---------------------------------------------------------------- | ||
120 | # Optional, if unspecified these will use the router defaults | ||
121 | # ---------------------------------------------------------------- | ||
122 | #: List of content-types supported by this route | ||
123 | content_types = None | ||
124 | |||
125 | #: Source for authentication | ||
126 | auth_source = None | ||
127 | |||
128 | #: Source for authorization | ||
129 | authz_source = None | ||
130 | |||
131 | |||
132 | class FragmentSpec(object): | ||
133 | |||
134 | #: Indicates if the framgent is required | ||
135 | required = True | ||
136 | |||
137 | #: Regex used to check if the fragment is valid if specified | ||
138 | #: value_list may not be specified. | ||
139 | regex = r'.*' | ||
140 | |||
141 | #: Optional list of acceptable values. May not be specified | ||
142 | #: with regex | ||
143 | value_list = None | ||
144 | |||
145 | #: Default value if the user provides no value | ||
146 | default = None | ||
147 | |||
148 | |||
149 | class NotSupported(object): | ||
150 | pass | ||
151 | |||
152 | |||
153 | class Resource(object): | ||
154 | |||
155 | GET = NotSupported() | ||
156 | POST = NotSupported() | ||
157 | HEAD = NotSupported() | ||
158 | PUT = NotSupported() | ||
159 | DELETE = NotSupported() | ||
diff --git a/foundry/utils.py b/foundry/utils.py new file mode 100644 index 0000000..b5d58b6 --- /dev/null +++ b/foundry/utils.py | |||
@@ -0,0 +1,106 @@ | |||
1 | # vim: set filencoding=utf8 | ||
2 | """ | ||
3 | Random Stuff | ||
4 | |||
5 | @author: Mike Crute (mcrute@gmail.com) | ||
6 | @organization: SoftGroup Interactive, Inc. | ||
7 | @date: May 02, 2010 | ||
8 | |||
9 | Random stuff that doesn't deserve it's own module | ||
10 | but is still needed for the rest of the program. | ||
11 | """ | ||
12 | |||
13 | class frozendict(dict): | ||
14 | """ | ||
15 | A frozen dictionary implementation can not be modified once | ||
16 | it has been constructed, much like a tuple. Frozen dictionaries | ||
17 | are hashable. | ||
18 | """ | ||
19 | |||
20 | def __new__(cls, indict): | ||
21 | inst = dict.__new__(cls) | ||
22 | inst.__hash = hash(tuple(sorted(indict.items()))) | ||
23 | inst.__slots__ = indict.keys() | ||
24 | dict.__init__(inst, indict) | ||
25 | return inst | ||
26 | |||
27 | @property | ||
28 | def __blocked(self): | ||
29 | raise AttributeError("Can't modify frozendict instance.") | ||
30 | |||
31 | __delitem__ = __setitem__ = clear = pop = __blocked | ||
32 | popitem = setdefault = update = __blocked | ||
33 | |||
34 | __hash__ = lambda self: self.__hash | ||
35 | __repr__ = lambda self: "frozendict({1})".format(dict.__repr__(self)) | ||
36 | |||
37 | |||
38 | def implements(interface, debug=False): | ||
39 | """ | ||
40 | Verify that a class conforms to a specified interface. | ||
41 | This decorator is not perfect, for example it can not | ||
42 | check exceptions or return values. But it does ensure | ||
43 | that all public methods exist and their arguments | ||
44 | conform to the interface. | ||
45 | |||
46 | The debug flag allows overriding checking of the runtime | ||
47 | flag for testing purposes, it should never be set in | ||
48 | production code. | ||
49 | |||
50 | NOTE: This decorator does nothing if -d is not passed | ||
51 | to the Python runtime. | ||
52 | """ | ||
53 | import sys | ||
54 | if not sys.flags.debug and not debug: | ||
55 | return lambda func: func | ||
56 | |||
57 | # Defer this import until we know we're supposed to run | ||
58 | import inspect | ||
59 | |||
60 | def get_filtered_members(item): | ||
61 | "Gets non-private or non-protected symbols." | ||
62 | return dict([(key, value) for key, value in inspect.getmembers(item) | ||
63 | if not key.startswith('_')]) | ||
64 | |||
65 | def build_contract(item): | ||
66 | """ | ||
67 | Builds a function contract string. The contract | ||
68 | string will ignore the name of positional params | ||
69 | but will consider the name of keyword arguments. | ||
70 | """ | ||
71 | argspec = inspect.getargspec(item) | ||
72 | |||
73 | if argspec.defaults: | ||
74 | num_keywords = len(argspec.defaults) | ||
75 | args = ['_'] * (len(argspec.args) - num_keywords) | ||
76 | args.extend(argspec.args[num_keywords-1:]) | ||
77 | else: | ||
78 | args = ['_'] * len(argspec.args) | ||
79 | |||
80 | if argspec.varargs: | ||
81 | args.append('*args') | ||
82 | |||
83 | if argspec.keywords: | ||
84 | args.append('**kwargs') | ||
85 | |||
86 | return ', '.join(args) | ||
87 | |||
88 | def tester(klass): | ||
89 | "Verifies conformance to the interface." | ||
90 | interface_elements = get_filtered_members(interface) | ||
91 | class_elements = get_filtered_members(klass) | ||
92 | |||
93 | for key, value in interface_elements.items(): | ||
94 | assert key in class_elements, \ | ||
95 | "{0!r} is required but missing.".format(key) | ||
96 | |||
97 | if inspect.isfunction(value) or inspect.ismethod(value): | ||
98 | contract = build_contract(value) | ||
99 | implementation = build_contract(class_elements[key]) | ||
100 | |||
101 | assert implementation == contract, \ | ||
102 | "{0!r} doesn't conform to interface.".format(key) | ||
103 | |||
104 | return klass | ||
105 | |||
106 | return tester | ||
diff --git a/foundry/vcs/__init__.py b/foundry/vcs/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/foundry/vcs/__init__.py | |||
diff --git a/foundry/vcs/hg/__init__.py b/foundry/vcs/hg/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/foundry/vcs/hg/__init__.py | |||
diff --git a/foundry/vcs/hg/providers.py b/foundry/vcs/hg/providers.py new file mode 100644 index 0000000..232464e --- /dev/null +++ b/foundry/vcs/hg/providers.py | |||
@@ -0,0 +1,24 @@ | |||
1 | # vim: set filencoding=utf8 | ||
2 | """ | ||
3 | SnakeGuice Providers | ||
4 | |||
5 | @author: Mike Crute (mcrute@gmail.com) | ||
6 | @organization: SoftGroup Interactive, Inc. | ||
7 | @date: May 02, 2010 | ||
8 | """ | ||
9 | |||
10 | from mercurial import hg | ||
11 | from mercurial.ui import ui | ||
12 | from foundry.utils import implements | ||
13 | from foundry.interfaces import RepositoryProvider | ||
14 | |||
15 | |||
16 | @implements(RepositoryProvider) | ||
17 | class RepoProvider(object): | ||
18 | |||
19 | def get(self, repo_path=''): | ||
20 | u = ui() | ||
21 | u.setconfig('ui', 'report_untrusted', 'off') | ||
22 | u.setconfig('ui', 'interactive', 'false') | ||
23 | |||
24 | return hg.repository(u, repo_path) | ||
diff --git a/foundry/views/__init__.py b/foundry/views/__init__.py new file mode 100644 index 0000000..dd26dfe --- /dev/null +++ b/foundry/views/__init__.py | |||
@@ -0,0 +1,23 @@ | |||
1 | # vim: set filencoding=utf8 | ||
2 | """ | ||
3 | Template Renderer | ||
4 | |||
5 | @author: Mike Crute (mcrute@gmail.com) | ||
6 | @organization: SoftGroup Interactive, Inc. | ||
7 | @date: May 03, 2010 | ||
8 | """ | ||
9 | |||
10 | |||
11 | from foundry import interfaces | ||
12 | from foundry.utils import implements | ||
13 | |||
14 | |||
15 | @implements(interfaces.TemplateRenderer) | ||
16 | class JinjaRenderer(object): | ||
17 | |||
18 | def __init__(self, tpl_env): | ||
19 | self.tpl_env = tpl_env | ||
20 | |||
21 | def render(self, template, *args, **kwargs): | ||
22 | template = self.tpl_env.get_template(template) | ||
23 | return template.render(*args, **kwargs) | ||
diff --git a/foundry/views/changelog.tpl b/foundry/views/changelog.tpl new file mode 100644 index 0000000..f6d3a75 --- /dev/null +++ b/foundry/views/changelog.tpl | |||
@@ -0,0 +1,42 @@ | |||
1 | <style type="text/css"> | ||
2 | .branchtag, .tagtag | ||
3 | { | ||
4 | border: 1px solid; | ||
5 | padding: 2px 6px; | ||
6 | } | ||
7 | |||
8 | .branchtag | ||
9 | { | ||
10 | background-color: #AFA; | ||
11 | border-color: #CFC #0C3 #0C3 #CFC; | ||
12 | } | ||
13 | |||
14 | .tagtag | ||
15 | { | ||
16 | background-color: #FFA; | ||
17 | border-color: #FFC #FE0 #FE0 #FFC; | ||
18 | } | ||
19 | </style> | ||
20 | |||
21 | <table> | ||
22 | <tr> | ||
23 | <th>Changed</th> | ||
24 | <th>User</th> | ||
25 | <th>Summary</th> | ||
26 | </tr> | ||
27 | {% for changeset in repo %} | ||
28 | <tr> | ||
29 | <td>{{ changeset.date()|nice_date_delta }}</td> | ||
30 | <td>{{ changeset.user() }}</td> | ||
31 | <td> | ||
32 | {{ changeset.description() }} | ||
33 | {% for tag in changeset.tags() %} | ||
34 | <span class="tagtag">{{ tag }}</span> | ||
35 | {% endfor %} | ||
36 | {% if changeset.branch() != 'default' %} | ||
37 | <span class="branchtag">{{ changeset.branch() }}</span> | ||
38 | {% endif %} | ||
39 | </td> | ||
40 | </tr> | ||
41 | {% endfor %} | ||
42 | </table> | ||
diff --git a/setup.py b/setup.py new file mode 100755 index 0000000..eacf87f --- /dev/null +++ b/setup.py | |||
@@ -0,0 +1,18 @@ | |||
1 | #!/usr/bin/env python | ||
2 | from setuptools import setup | ||
3 | |||
4 | setup( | ||
5 | name="Foundry", | ||
6 | description="", | ||
7 | author="Mike Crute", | ||
8 | author_email="mcrute@gmail.com", | ||
9 | license="BSD", | ||
10 | version="0.1", | ||
11 | zip_safe=False, | ||
12 | include_package_data=True, | ||
13 | install_requires=[ | ||
14 | "Routes", | ||
15 | "Webob", | ||
16 | "Jinja2", | ||
17 | "SQLAlchemy", | ||
18 | ]) | ||
diff --git a/tests/test_frozendict.py b/tests/test_frozendict.py new file mode 100644 index 0000000..3f8045d --- /dev/null +++ b/tests/test_frozendict.py | |||
@@ -0,0 +1,19 @@ | |||
1 | from foundry.utils import frozendict | ||
2 | from nose.tools import raises | ||
3 | |||
4 | |||
5 | class TestFrozenDict(object): | ||
6 | |||
7 | def setup(self): | ||
8 | self.inputs = { 'foo': 'bar' } | ||
9 | self.dict_ = frozendict(self.inputs) | ||
10 | |||
11 | @raises(AttributeError) | ||
12 | def test_should_not_allow_assignment(self): | ||
13 | self.dict_['bar'] = 'baz' | ||
14 | |||
15 | def test_should_use_precomputed_hash(self): | ||
16 | assert hash(self.dict_) == self.dict_._frozendict__hash | ||
17 | |||
18 | def test_should_set_slots(self): | ||
19 | assert self.dict_.__slots__ == self.inputs.keys() | ||
diff --git a/tests/test_implements.py b/tests/test_implements.py new file mode 100644 index 0000000..48ffe4b --- /dev/null +++ b/tests/test_implements.py | |||
@@ -0,0 +1,51 @@ | |||
1 | # vim: set filencoding=utf8 | ||
2 | """ | ||
3 | Implements Decorator Tests | ||
4 | |||
5 | @author: Mike Crute (mcrute@gmail.com) | ||
6 | @organization: SoftGroup Interactive, Inc. | ||
7 | @date: May 02, 2010 | ||
8 | """ | ||
9 | |||
10 | |||
11 | from foundry.utils import implements | ||
12 | from nose.tools import raises | ||
13 | |||
14 | |||
15 | class MyInterface(object): | ||
16 | |||
17 | def get(self, foo): | ||
18 | pass | ||
19 | |||
20 | def set(self, bar, baz=None): | ||
21 | pass | ||
22 | |||
23 | def remove(self, *args, **kwargs): | ||
24 | pass | ||
25 | |||
26 | |||
27 | def test_conforming_should_not_fail(): | ||
28 | @implements(MyInterface, debug=True) | ||
29 | class Conforming(object): | ||
30 | |||
31 | def get(self, foo): | ||
32 | pass | ||
33 | |||
34 | def set(self, bar, baz=None): | ||
35 | pass | ||
36 | |||
37 | def remove(self, *args, **kwargs): | ||
38 | pass | ||
39 | |||
40 | |||
41 | @raises(AssertionError) | ||
42 | def test_non_conforming_should_fail(): | ||
43 | @implements(MyInterface, debug=True) | ||
44 | class NonConforming(object): | ||
45 | pass | ||
46 | |||
47 | |||
48 | def test_non_debug_should_do_nothing(): | ||
49 | @implements(MyInterface) | ||
50 | class NonConforming(object): | ||
51 | pass | ||