├── .gitignore
├── .travis.yml
├── BermiInflector
├── BSD-LICENSE
├── Inflector.py
├── README.markdown
├── Rules
│ ├── Base.py
│ ├── English.py
│ └── __init__.py
├── __init__.py
└── tests.py
├── LICENSE
├── Makefile
├── README.markdown
├── doc
├── examples.xhtml
├── index.xhtml
├── lowlevel.xhtml
├── relationship_examples.xhtml
├── stylesheet.css
└── template.tpl
├── setup.py
├── tox.ini
└── twistar
├── __init__.py
├── dbconfig
├── __init__.py
├── base.py
├── mysql.py
├── postgres.py
├── pyodbc.py
└── sqlite.py
├── dbobject.py
├── exceptions.py
├── registry.py
├── relationships.py
├── tests
├── __init__.py
├── mysql_config.py
├── postgres_config.py
├── sqlite_config.py
├── test_dbconfig.py
├── test_dbobject.py
├── test_relationships.py
├── test_transactions.py
├── test_utils.py
└── utils.py
├── utils.py
└── validation.py
/.gitignore:
--------------------------------------------------------------------------------
1 | _trial_temp*
2 | apidoc/*
3 | doc/*.html
4 | *.pyc
5 | _trial_temp
6 | build
7 | dist
8 | twistar.egg-info
9 | .tox
10 | .coverage
11 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: python
2 | sudo: false
3 |
4 | matrix:
5 | include:
6 | - env: TOXENV=py27-twisted12
7 | python: "2.7"
8 | - env: TOXENV=py27-twisted13
9 | python: "2.7"
10 | - env: TOXENV=py27-twisted14
11 | python: "2.7"
12 | - env: TOXENV=py27-twisted15
13 | python: "2.7"
14 | - env: TOXENV=py27-twisted16
15 | python: "2.7"
16 | - env: TOXENV=py35-twisted16
17 | python: "3.5"
18 | - env: TOXENV=pypy-twisted12
19 | python: "pypy"
20 | - env: TOXENV=pypy-twisted13
21 | python: "pypy"
22 | - env: TOXENV=pypy-twisted14
23 | python: "pypy"
24 | - env: TOXENV=pypy-twisted15
25 | python: "pypy"
26 | - env: TOXENV=pypy-twisted16
27 | python: "pypy"
28 | - env: TOXENV=pypy3-twisted16
29 | python: "pypy3"
30 | allow_failures:
31 | - env: TOXENV=pypy3-twisted16
32 |
33 | install:
34 | - pip install tox coveralls pep8 pyflakes
35 |
36 | script: make lint && tox
37 |
38 | after_success: coveralls
39 |
--------------------------------------------------------------------------------
/BermiInflector/BSD-LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2006 Bermi Ferrer Martinez
2 | Permission is hereby granted, free of charge, to any person obtaining a copy
3 | of this software to deal in this software without restriction, including
4 | without limitation the rights to use, copy, modify, merge, publish,
5 | distribute, sublicense, and/or sell copies of this software, and to permit
6 | persons to whom this software is furnished to do so, subject to the following
7 | condition:
8 |
9 | THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
10 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
11 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
12 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
13 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
14 | OUT OF OR IN CONNECTION WITH THIS SOFTWARE OR THE USE OR OTHER DEALINGS IN
15 | THIS SOFTWARE.
--------------------------------------------------------------------------------
/BermiInflector/Inflector.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | # Copyright (c) 2006 Bermi Ferrer Martinez
4 | #
5 | # bermi a-t bermilabs - com
6 | # See the end of this file for the free software, open source license (BSD-style).
7 |
8 | from __future__ import absolute_import
9 | import re
10 | from .Rules.English import English
11 |
12 | class Inflector :
13 | """
14 | Inflector for pluralizing and singularizing nouns.
15 |
16 | It provides methods for helping on creating programs
17 | based on naming conventions like on Ruby on Rails.
18 | """
19 |
20 | def __init__( self, Inflector = English ) :
21 | assert callable(Inflector), "Inflector should be a callable obj"
22 | self.Inflector = Inflector()
23 |
24 | def pluralize(self, word) :
25 | '''Pluralizes nouns.'''
26 | return self.Inflector.pluralize(word)
27 |
28 | def singularize(self, word) :
29 | '''Singularizes nouns.'''
30 | return self.Inflector.singularize(word)
31 |
32 | def conditionalPlural(self, numer_of_records, word) :
33 | '''Returns the plural form of a word if first parameter is greater than 1'''
34 | return self.Inflector.conditionalPlural(numer_of_records, word)
35 |
36 | def titleize(self, word, uppercase = '') :
37 | '''Converts an underscored or CamelCase word into a sentence.
38 | The titleize function converts text like "WelcomePage",
39 | "welcome_page" or "welcome page" to this "Welcome Page".
40 | If the "uppercase" parameter is set to 'first' it will only
41 | capitalize the first character of the title.'''
42 | return self.Inflector.titleize(word, uppercase)
43 |
44 | def camelize(self, word):
45 | ''' Returns given word as CamelCased
46 | Converts a word like "send_email" to "SendEmail". It
47 | will remove non alphanumeric character from the word, so
48 | "who's online" will be converted to "WhoSOnline"'''
49 | return self.Inflector.camelize(word)
50 |
51 | def underscore(self, word) :
52 | ''' Converts a word "into_it_s_underscored_version"
53 | Convert any "CamelCased" or "ordinary Word" into an
54 | "underscored_word".
55 | This can be really useful for creating friendly URLs.'''
56 | return self.Inflector.underscore(word)
57 |
58 | def humanize(self, word, uppercase = '') :
59 | '''Returns a human-readable string from word
60 | Returns a human-readable string from word, by replacing
61 | underscores with a space, and by upper-casing the initial
62 | character by default.
63 | If you need to uppercase all the words you just have to
64 | pass 'all' as a second parameter.'''
65 | return self.Inflector.humanize(word, uppercase)
66 |
67 |
68 | def variablize(self, word) :
69 | '''Same as camelize but first char is lowercased
70 | Converts a word like "send_email" to "sendEmail". It
71 | will remove non alphanumeric character from the word, so
72 | "who's online" will be converted to "whoSOnline"'''
73 | return self.Inflector.variablize(word)
74 |
75 | def tableize(self, class_name) :
76 | ''' Converts a class name to its table name according to rails
77 | naming conventions. Example. Converts "Person" to "people" '''
78 | return self.Inflector.tableize(class_name)
79 |
80 | def classify(self, table_name) :
81 | '''Converts a table name to its class name according to rails
82 | naming conventions. Example: Converts "people" to "Person" '''
83 | return self.Inflector.classify(table_name)
84 |
85 | def ordinalize(self, number) :
86 | '''Converts number to its ordinal form.
87 | This method converts 13 to 13th, 2 to 2nd ...'''
88 | return self.Inflector.ordinalize(number)
89 |
90 |
91 | def unaccent(self, text) :
92 | '''Transforms a string to its unaccented version.
93 | This might be useful for generating "friendly" URLs'''
94 | return self.Inflector.unaccent(text)
95 |
96 | def urlize(self, text) :
97 | '''Transform a string its unaccented and underscored
98 | version ready to be inserted in friendly URLs'''
99 | return self.Inflector.urlize(text)
100 |
101 |
102 | def demodulize(self, module_name) :
103 | return self.Inflector.demodulize(module_name)
104 |
105 | def modulize(self, module_description) :
106 | return self.Inflector.modulize(module_description)
107 |
108 | def foreignKey(self, class_name, separate_class_name_and_id_with_underscore = 1) :
109 | ''' Returns class_name in underscored form, with "_id" tacked on at the end.
110 | This is for use in dealing with the database.'''
111 | return self.Inflector.foreignKey(class_name, separate_class_name_and_id_with_underscore)
112 |
113 |
114 |
115 |
116 | # Copyright (c) 2006 Bermi Ferrer Martinez
117 | # Permission is hereby granted, free of charge, to any person obtaining a copy
118 | # of this software to deal in this software without restriction, including
119 | # without limitation the rights to use, copy, modify, merge, publish,
120 | # distribute, sublicense, and/or sell copies of this software, and to permit
121 | # persons to whom this software is furnished to do so, subject to the following
122 | # condition:
123 | #
124 | # THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
125 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
126 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
127 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
128 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
129 | # OUT OF OR IN CONNECTION WITH THIS SOFTWARE OR THE USE OR OTHER DEALINGS IN
130 | # THIS SOFTWARE.
131 |
--------------------------------------------------------------------------------
/BermiInflector/README.markdown:
--------------------------------------------------------------------------------
1 | # Inflector for Python
2 |
3 | The Inflector is used for getting the plural and singular form of nouns. This piece of code helps on creating code that favors convention over configuration.
4 |
5 | Only English and Spanish nouns are supported. The English version is a port of Ruby on Rails Inflector, while the Spanish Version has been developed from scratch with the help of Carles Sadurní.
6 |
7 | Apart from converting singulars and plurals, this module also handles necessary string conversion for convention based applications like:
8 |
9 | Available methods are:
10 |
11 | ## pluralize(word)
12 |
13 | Pluralizes nouns.
14 |
15 | ## singularize(word)
16 |
17 | Singularizes nouns.
18 |
19 | ## conditionalPlural(numer_of_records, word)
20 |
21 | Returns the plural form of a word if first parameter is greater than 1
22 |
23 | ## titleize(word, uppercase = '')
24 |
25 | Converts an underscored or CamelCase word into a sentence.
26 | The titleize function converts text like "WelcomePage",
27 | "welcome_page" or "welcome page" to this "Welcome Page".
28 | If the "uppercase" parameter is set to 'first' it will only
29 | capitalize the first character of the title.
30 |
31 | ## camelize(word):
32 |
33 | Returns given word as CamelCased
34 | Converts a word like "send_email" to "SendEmail". It
35 | will remove non alphanumeric character from the word, so
36 | "who's online" will be converted to "WhoSOnline"
37 |
38 | ## underscore(word)
39 |
40 | Converts a word "into_it_s_underscored_version"
41 | Convert any "CamelCased" or "ordinary Word" into an
42 | "underscored_word".
43 | This can be really useful for creating friendly URLs.
44 |
45 | ## humanize(word, uppercase = '')
46 |
47 | Returns a human-readable string from word
48 | Returns a human-readable string from word, by replacing
49 | underscores with a space, and by upper-casing the initial
50 | character by default.
51 | If you need to uppercase all the words you just have to
52 | pass 'all' as a second parameter.
53 |
54 |
55 | ## variablize(word)
56 |
57 | Same as camelize but first char is lowercased
58 | Converts a word like "send_email" to "sendEmail". It
59 | will remove non alphanumeric character from the word, so
60 | "who's online" will be converted to "whoSOnline"
61 | return self.Inflector.variablize(word)
62 |
63 | ## tableize(class_name)
64 |
65 | Converts a class name to its table name according to rails
66 | naming conventions. Example. Converts "Person" to "people"
67 |
68 | ## classify(table_name)
69 |
70 | Converts a table name to its class name according to rails
71 | naming conventions. Example: Converts "people" to "Person"
72 |
73 | ## ordinalize(number)
74 | Converts number to its ordinal form.
75 | This method converts 13 to 13th, 2 to 2nd ...
76 |
77 | ## unaccent(text)
78 |
79 | Transforms a string to its unaccented version.
80 | This might be useful for generating "friendly" URLs
81 |
82 | ## urlize(text)
83 |
84 | Transform a string its unaccented and underscored
85 | version ready to be inserted in friendly URLs
86 |
87 | ## foreignKey(class_name, separate_class_name_and_id_with_underscore = 1)
88 |
89 | Returns class_name in underscored form, with "_id" tacked on at the end.
90 | This is for use in dealing with the database.
91 |
--------------------------------------------------------------------------------
/BermiInflector/Rules/Base.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | # Copyright (c) 2006 Bermi Ferrer Martinez
4 | # bermi a-t bermilabs - com
5 | # See the end of this file for the free software, open source license (BSD-style).
6 |
7 | from __future__ import absolute_import
8 | import re
9 | from six.moves import range
10 |
11 | class Base:
12 | '''Locale inflectors must inherit from this base class inorder to provide
13 | the basic Inflector functionality'''
14 |
15 | def conditionalPlural(self, numer_of_records, word) :
16 | '''Returns the plural form of a word if first parameter is greater than 1'''
17 |
18 | if numer_of_records > 1 :
19 | return self.pluralize(word)
20 | else :
21 | return word
22 |
23 |
24 | def titleize(self, word, uppercase = '') :
25 | '''Converts an underscored or CamelCase word into a English sentence.
26 | The titleize function converts text like "WelcomePage",
27 | "welcome_page" or "welcome page" to this "Welcome Page".
28 | If second parameter is set to 'first' it will only
29 | capitalize the first character of the title.'''
30 |
31 | if(uppercase == 'first'):
32 | return self.humanize(self.underscore(word)).capitalize()
33 | else :
34 | return self.humanize(self.underscore(word)).title()
35 |
36 |
37 | def camelize(self, word):
38 | ''' Returns given word as CamelCased
39 | Converts a word like "send_email" to "SendEmail". It
40 | will remove non alphanumeric character from the word, so
41 | "who's online" will be converted to "WhoSOnline"'''
42 | return ''.join(w[0].upper() + w[1:] for w in re.sub('[^A-Z^a-z^0-9^:]+', ' ', word).split(' '))
43 |
44 | def underscore(self, word) :
45 | ''' Converts a word "into_it_s_underscored_version"
46 | Convert any "CamelCased" or "ordinary Word" into an
47 | "underscored_word".
48 | This can be really useful for creating friendly URLs.'''
49 |
50 | return re.sub('[^A-Z^a-z^0-9^\/]+','_', \
51 | re.sub('([a-z\d])([A-Z])','\\1_\\2', \
52 | re.sub('([A-Z]+)([A-Z][a-z])','\\1_\\2', re.sub('::', '/',word)))).lower()
53 |
54 |
55 | def humanize(self, word, uppercase = '') :
56 | '''Returns a human-readable string from word
57 | Returns a human-readable string from word, by replacing
58 | underscores with a space, and by upper-casing the initial
59 | character by default.
60 | If you need to uppercase all the words you just have to
61 | pass 'all' as a second parameter.'''
62 |
63 | if(uppercase == 'first'):
64 | return re.sub('_id$', '', word).replace('_',' ').capitalize()
65 | else :
66 | return re.sub('_id$', '', word).replace('_',' ').title()
67 |
68 |
69 | def variablize(self, word) :
70 | '''Same as camelize but first char is lowercased
71 | Converts a word like "send_email" to "sendEmail". It
72 | will remove non alphanumeric character from the word, so
73 | "who's online" will be converted to "whoSOnline"'''
74 | word = self.camelize(word)
75 | return word[0].lower()+word[1:]
76 |
77 | def tableize(self, class_name) :
78 | ''' Converts a class name to its table name according to rails
79 | naming conventions. Example. Converts "Person" to "people" '''
80 | return self.pluralize(self.underscore(class_name))
81 |
82 |
83 | def classify(self, table_name) :
84 | '''Converts a table name to its class name according to rails
85 | naming conventions. Example: Converts "people" to "Person" '''
86 | return self.camelize(self.singularize(table_name))
87 |
88 |
89 | def ordinalize(self, number) :
90 | '''Converts number to its ordinal English form.
91 | This method converts 13 to 13th, 2 to 2nd ...'''
92 | tail = 'th'
93 | if number % 100 == 11 or number % 100 == 12 or number % 100 == 13:
94 | tail = 'th'
95 | elif number % 10 == 1 :
96 | tail = 'st'
97 | elif number % 10 == 2 :
98 | tail = 'nd'
99 | elif number % 10 == 3 :
100 | tail = 'rd'
101 |
102 | return str(number)+tail
103 |
104 |
105 | def unaccent(self, text) :
106 | '''Transforms a string to its unaccented version.
107 | This might be useful for generating "friendly" URLs'''
108 | find = u'\u00C0\u00C1\u00C2\u00C3\u00C4\u00C5\u00C6\u00C7\u00C8\u00C9\u00CA\u00CB\u00CC\u00CD\u00CE\u00CF\u00D0\u00D1\u00D2\u00D3\u00D4\u00D5\u00D6\u00D8\u00D9\u00DA\u00DB\u00DC\u00DD\u00DE\u00DF\u00E0\u00E1\u00E2\u00E3\u00E4\u00E5\u00E6\u00E7\u00E8\u00E9\u00EA\u00EB\u00EC\u00ED\u00EE\u00EF\u00F0\u00F1\u00F2\u00F3\u00F4\u00F5\u00F6\u00F8\u00F9\u00FA\u00FB\u00FC\u00FD\u00FE\u00FF'
109 | replace = u'AAAAAAACEEEEIIIIDNOOOOOOUUUUYTsaaaaaaaceeeeiiiienoooooouuuuyty'
110 | return self.string_replace(text, find, replace)
111 |
112 | def string_replace (self, word, find, replace) :
113 | '''This function returns a copy of word, translating
114 | all occurrences of each character in find to the
115 | corresponding character in replace'''
116 | for k in range(0,len(find)) :
117 | word = re.sub(find[k], replace[k], word)
118 |
119 | return word
120 |
121 | def urlize(self, text) :
122 | '''Transform a string its unaccented and underscored
123 | version ready to be inserted in friendly URLs'''
124 | return re.sub('^_|_$','',self.underscore(self.unaccent(text)))
125 |
126 |
127 | def demodulize(self, module_name) :
128 | return self.humanize(self.underscore(re.sub('^.*::','',module_name)))
129 |
130 | def modulize(self, module_description) :
131 | return self.camelize(self.singularize(module_description))
132 |
133 | def foreignKey(self, class_name, separate_class_name_and_id_with_underscore = 1) :
134 | ''' Returns class_name in underscored form, with "_id" tacked on at the end.
135 | This is for use in dealing with the database.'''
136 | if separate_class_name_and_id_with_underscore :
137 | tail = '_id'
138 | else :
139 | tail = 'id'
140 | return self.underscore(self.demodulize(class_name))+tail;
141 |
142 |
143 |
144 | # Copyright (c) 2006 Bermi Ferrer Martinez
145 | # Permission is hereby granted, free of charge, to any person obtaining a copy
146 | # of this software to deal in this software without restriction, including
147 | # without limitation the rights to use, copy, modify, merge, publish,
148 | # distribute, sublicense, and/or sell copies of this software, and to permit
149 | # persons to whom this software is furnished to do so, subject to the following
150 | # condition:
151 | #
152 | # THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
153 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
154 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
155 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
156 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
157 | # OUT OF OR IN CONNECTION WITH THIS SOFTWARE OR THE USE OR OTHER DEALINGS IN
158 | # THIS SOFTWARE.
--------------------------------------------------------------------------------
/BermiInflector/Rules/English.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | # Copyright (c) 2006 Bermi Ferrer Martinez
4 | # bermi a-t bermilabs - com
5 | #
6 | # See the end of this file for the free software, open source license (BSD-style).
7 |
8 | from __future__ import absolute_import
9 | import re
10 | from .Base import Base
11 | from six.moves import range
12 |
13 | class English (Base):
14 | """
15 | Inflector for pluralize and singularize English nouns.
16 |
17 | This is the default Inflector for the Inflector obj
18 | """
19 |
20 | def pluralize(self, word) :
21 | '''Pluralizes English nouns.'''
22 |
23 | rules = [
24 | ['(?i)(quiz)$' , '\\1zes'],
25 | ['^(?i)(ox)$' , '\\1en'],
26 | ['(?i)([m|l])ouse$' , '\\1ice'],
27 | ['(?i)(matr|vert|ind)ix|ex$' , '\\1ices'],
28 | ['(?i)(x|ch|ss|sh)$' , '\\1es'],
29 | ['(?i)([^aeiouy]|qu)ies$' , '\\1y'],
30 | ['(?i)([^aeiouy]|qu)y$' , '\\1ies'],
31 | ['(?i)(hive)$' , '\\1s'],
32 | ['(?i)(?:([^f])fe|([lr])f)$' , '\\1\\2ves'],
33 | ['(?i)sis$' , 'ses'],
34 | ['(?i)([ti])um$' , '\\1a'],
35 | ['(?i)(buffal|tomat)o$' , '\\1oes'],
36 | ['(?i)(bu)s$' , '\\1ses'],
37 | ['(?i)(alias|status)' , '\\1es'],
38 | ['(?i)(octop|vir)us$' , '\\1i'],
39 | ['(?i)(ax|test)is$' , '\\1es'],
40 | ['(?i)s$' , 's'],
41 | ['(?i)$' , 's']
42 | ]
43 |
44 | uncountable_words = ['equipment', 'information', 'rice', 'money', 'species', 'series', 'fish', 'sheep']
45 |
46 | irregular_words = {
47 | 'person' : 'people',
48 | 'man' : 'men',
49 | 'child' : 'children',
50 | 'sex' : 'sexes',
51 | 'move' : 'moves'
52 | }
53 |
54 | lower_cased_word = word.lower();
55 |
56 | for uncountable_word in uncountable_words:
57 | if lower_cased_word[-1*len(uncountable_word):] == uncountable_word :
58 | return word
59 |
60 | for irregular in irregular_words.keys():
61 | match = re.search('('+irregular+')$',word, re.IGNORECASE)
62 | if match:
63 | return re.sub('(?i)'+irregular+'$', match.expand('\\1')[0]+irregular_words[irregular][1:], word)
64 |
65 | for rule in range(len(rules)):
66 | match = re.search(rules[rule][0], word, re.IGNORECASE)
67 | if match :
68 | groups = match.groups()
69 | for k in range(0,len(groups)) :
70 | if groups[k] == None :
71 | rules[rule][1] = rules[rule][1].replace('\\'+str(k+1), '')
72 |
73 | return re.sub(rules[rule][0], rules[rule][1], word)
74 |
75 | return word
76 |
77 |
78 | def singularize (self, word) :
79 | '''Singularizes English nouns.'''
80 |
81 | rules = [
82 | ['(?i)(quiz)zes$' , '\\1'],
83 | ['(?i)(matr)ices$' , '\\1ix'],
84 | ['(?i)(vert|ind)ices$' , '\\1ex'],
85 | ['(?i)^(ox)en' , '\\1'],
86 | ['(?i)(alias|status)es$' , '\\1'],
87 | ['(?i)([octop|vir])i$' , '\\1us'],
88 | ['(?i)(cris|ax|test)es$' , '\\1is'],
89 | ['(?i)(shoe)s$' , '\\1'],
90 | ['(?i)(o)es$' , '\\1'],
91 | ['(?i)(bus)es$' , '\\1'],
92 | ['(?i)([m|l])ice$' , '\\1ouse'],
93 | ['(?i)(x|ch|ss|sh)es$' , '\\1'],
94 | ['(?i)(m)ovies$' , '\\1ovie'],
95 | ['(?i)(s)eries$' , '\\1eries'],
96 | ['(?i)([^aeiouy]|qu)ies$' , '\\1y'],
97 | ['(?i)([lr])ves$' , '\\1f'],
98 | ['(?i)(tive)s$' , '\\1'],
99 | ['(?i)(hive)s$' , '\\1'],
100 | ['(?i)([^f])ves$' , '\\1fe'],
101 | ['(?i)(^analy)ses$' , '\\1sis'],
102 | ['(?i)((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)ses$' , '\\1\\2sis'],
103 | ['(?i)([ti])a$' , '\\1um'],
104 | ['(?i)(n)ews$' , '\\1ews'],
105 | ['(?i)s$' , ''],
106 | ];
107 |
108 | uncountable_words = ['equipment', 'information', 'rice', 'money', 'species', 'series', 'fish', 'sheep','sms'];
109 |
110 | irregular_words = {
111 | 'people' : 'person',
112 | 'men' : 'man',
113 | 'children' : 'child',
114 | 'sexes' : 'sex',
115 | 'moves' : 'move'
116 | }
117 |
118 | lower_cased_word = word.lower();
119 |
120 | for uncountable_word in uncountable_words:
121 | if lower_cased_word[-1*len(uncountable_word):] == uncountable_word :
122 | return word
123 |
124 | for irregular in irregular_words.keys():
125 | match = re.search('('+irregular+')$',word, re.IGNORECASE)
126 | if match:
127 | return re.sub('(?i)'+irregular+'$', match.expand('\\1')[0]+irregular_words[irregular][1:], word)
128 |
129 |
130 | for rule in range(len(rules)):
131 | match = re.search(rules[rule][0], word, re.IGNORECASE)
132 | if match :
133 | groups = match.groups()
134 | for k in range(0,len(groups)) :
135 | if groups[k] == None :
136 | rules[rule][1] = rules[rule][1].replace('\\'+str(k+1), '')
137 |
138 | return re.sub(rules[rule][0], rules[rule][1], word)
139 |
140 | return word
141 |
142 |
143 |
144 | # Copyright (c) 2006 Bermi Ferrer Martinez
145 | # Permission is hereby granted, free of charge, to any person obtaining a copy
146 | # of this software to deal in this software without restriction, including
147 | # without limitation the rights to use, copy, modify, merge, publish,
148 | # distribute, sublicense, and/or sell copies of this software, and to permit
149 | # persons to whom this software is furnished to do so, subject to the following
150 | # condition:
151 | #
152 | # THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
153 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
154 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
155 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
156 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
157 | # OUT OF OR IN CONNECTION WITH THIS SOFTWARE OR THE USE OR OTHER DEALINGS IN
158 | # THIS SOFTWARE.
--------------------------------------------------------------------------------
/BermiInflector/Rules/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bmuller/twistar/1eb46ff2577473e0a26932ee57473e26203a3db2/BermiInflector/Rules/__init__.py
--------------------------------------------------------------------------------
/BermiInflector/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bmuller/twistar/1eb46ff2577473e0a26932ee57473e26203a3db2/BermiInflector/__init__.py
--------------------------------------------------------------------------------
/BermiInflector/tests.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 |
4 | # Copyright (c) 2006 Bermi Ferrer Martinez
5 | #
6 | # bermi a-t bermilabs - com
7 | #
8 | from __future__ import absolute_import
9 | import unittest
10 | from .Inflector import Inflector, English, Spanish
11 |
12 | class EnglishInflectorTestCase(unittest.TestCase):
13 | singular_to_plural = {
14 | "search" : "searches",
15 | "switch" : "switches",
16 | "fix" : "fixes",
17 | "box" : "boxes",
18 | "process" : "processes",
19 | "address" : "addresses",
20 | "case" : "cases",
21 | "stack" : "stacks",
22 | "wish" : "wishes",
23 | "fish" : "fish",
24 |
25 | "category" : "categories",
26 | "query" : "queries",
27 | "ability" : "abilities",
28 | "agency" : "agencies",
29 | "movie" : "movies",
30 |
31 | "archive" : "archives",
32 |
33 | "index" : "indices",
34 |
35 | "wife" : "wives",
36 | "safe" : "saves",
37 | "half" : "halves",
38 |
39 | "move" : "moves",
40 |
41 | "salesperson" : "salespeople",
42 | "person" : "people",
43 |
44 | "spokesman" : "spokesmen",
45 | "man" : "men",
46 | "woman" : "women",
47 |
48 | "basis" : "bases",
49 | "diagnosis" : "diagnoses",
50 |
51 | "datum" : "data",
52 | "medium" : "media",
53 | "analysis" : "analyses",
54 |
55 | "node_child" : "node_children",
56 | "child" : "children",
57 |
58 | "experience" : "experiences",
59 | "day" : "days",
60 |
61 | "comment" : "comments",
62 | "foobar" : "foobars",
63 | "newsletter" : "newsletters",
64 |
65 | "old_news" : "old_news",
66 | "news" : "news",
67 |
68 | "series" : "series",
69 | "species" : "species",
70 |
71 | "quiz" : "quizzes",
72 |
73 | "perspective" : "perspectives",
74 |
75 | "ox" : "oxen",
76 | "photo" : "photos",
77 | "buffalo" : "buffaloes",
78 | "tomato" : "tomatoes",
79 | "dwarf" : "dwarves",
80 | "elf" : "elves",
81 | "information" : "information",
82 | "equipment" : "equipment",
83 | "bus" : "buses",
84 | "status" : "statuses",
85 | "mouse" : "mice",
86 |
87 | "louse" : "lice",
88 | "house" : "houses",
89 | "octopus" : "octopi",
90 | "virus" : "viri",
91 | "alias" : "aliases",
92 | "portfolio" : "portfolios",
93 |
94 | "vertex" : "vertices",
95 | "matrix" : "matrices",
96 |
97 | "axis" : "axes",
98 | "testis" : "testes",
99 | "crisis" : "crises",
100 |
101 | "rice" : "rice",
102 | "shoe" : "shoes",
103 |
104 | "horse" : "horses",
105 | "prize" : "prizes",
106 | "edge" : "edges"
107 | }
108 | def setUp(self):
109 | self.inflector = Inflector(English)
110 |
111 | def tearDown(self):
112 | self.inflector = None
113 |
114 | def test_pluralize(self) :
115 | for singular in self.singular_to_plural.keys() :
116 | assert self.inflector.pluralize(singular) == self.singular_to_plural[singular], \
117 | 'English Inlector pluralize(%s) should produce "%s" and NOT "%s"' % (singular, self.singular_to_plural[singular], self.inflector.pluralize(singular))
118 |
119 | def test_singularize(self) :
120 | for singular in self.singular_to_plural.keys() :
121 | assert self.inflector.singularize(self.singular_to_plural[singular]) == singular, \
122 | 'English Inlector singularize(%s) should produce "%s" and NOT "%s"' % (self.singular_to_plural[singular], singular, self.inflector.singularize(self.singular_to_plural[singular]))
123 |
124 |
125 |
126 | InflectorTestSuite = unittest.TestSuite()
127 | InflectorTestSuite.addTest(EnglishInflectorTestCase("test_pluralize"))
128 | InflectorTestSuite.addTest(EnglishInflectorTestCase("test_singularize"))
129 | runner = unittest.TextTestRunner()
130 | runner.run(InflectorTestSuite)
131 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The files in the BermiInflector directory are released under the
2 | license described in the file BSD-LICENSE found in that folder.
3 | All other code is released with the following copyright and under
4 | the following license:
5 |
6 | Copyright (c) 2010 Butterfat, LLC
7 |
8 | Permission is hereby granted, free of charge, to any person obtaining
9 | a copy of this software and associated documentation files (the
10 | "Software"), to deal in the Software without restriction, including
11 | without limitation the rights to use, copy, modify, merge, publish,
12 | distribute, sublicense, and/or sell copies of the Software, and to
13 | permit persons to whom the Software is furnished to do so, subject to
14 | the following conditions:
15 |
16 | The above copyright notice and this permission notice shall be
17 | included in all copies or substantial portions of the Software.
18 |
19 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
20 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
21 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
22 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
23 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
24 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
25 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
26 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | PYDOCTOR=pydoctor
2 |
3 | docs:
4 | $(PYDOCTOR) --make-html --html-output apidoc --add-package twistar --project-name=twistar --project-url=http://findingscience.com/twistar --html-use-sorttable --html-use-splitlinks --html-shorten-lists
5 | lore --config template=doc/template.tpl doc/*.xhtml
6 |
7 | test:
8 | trial twistar
9 |
10 | install:
11 | python setup.py install
12 |
13 | lint:
14 | pep8 --ignore=E303 --max-line-length=140 ./twistar
15 | find ./twistar -name '*.py' | xargs pyflakes
16 |
--------------------------------------------------------------------------------
/README.markdown:
--------------------------------------------------------------------------------
1 | # twistar: Asynchronous Python ORM
2 | [](https://travis-ci.org/bmuller/twistar) [](https://coveralls.io/github/bmuller/twistar?branch=master)
3 |
4 | The Twistar Project provides an ActiveRecord (ORM) pattern interface to the Twisted Project's RDBMS library. This file contains minimal documentation - see the project home page at http://findingscience.com/twistar for more information.
5 |
6 | ## Installation
7 |
8 | ```
9 | pip install twistar
10 | ```
11 |
12 | ## Usage
13 | Your database must be one of: MySQL, PostgreSQL, or SQLite. The only DBAPI modules supported by Twistar are: MySQLdb, psycopg2, and sqlite3 - at least one of these must be installed.
14 |
15 | Here's the obligatory TL;DR example of creating a User record, assuming that there is a table named "users" with varchar columns for first_name and last_name and an int age column:
16 |
17 | ```python
18 | #!/usr/bin/env python
19 | from twisted.enterprise import adbapi
20 | from twistar.registry import Registry
21 | from twistar.dbobject import DBObject
22 | from twisted.internet import reactor
23 |
24 | class User(DBObject):
25 | pass
26 |
27 | def done(user):
28 | print "A user was just created with the name %s" % user.first_name
29 | reactor.stop()
30 |
31 | # Connect to the DB
32 | Registry.DBPOOL = adbapi.ConnectionPool('MySQLdb', user="twistar", passwd="apass", db="twistar")
33 |
34 | # make a user
35 | u = User()
36 | u.first_name = "John"
37 | u.last_name = "Smith"
38 | u.age = 25
39 |
40 | # Or, use this shorter version:
41 | u = User(first_name="John", last_name="Smith", age=25)
42 |
43 | # save the user
44 | u.save().addCallback(done)
45 |
46 | reactor.run()
47 | ```
48 |
49 | Then, finding this user is easy:
50 |
51 | ```python
52 | def found(users):
53 | print "I found %i users!" % len(users)
54 | for user in users:
55 | print "User: %s %s" % (user.first_name, user.last_name)
56 |
57 | u = User.findBy(first_name="John", age=25).addCallback(found)
58 | ```
59 |
60 | This is a very simple example - see http://findingscience.com/twistar for more complicated examples and additional uses.
61 |
62 | ## Testing
63 | To run unit-tests you simply require [Tox](https://tox.readthedocs.org)
64 |
65 | To run the tests:
66 | ```
67 | tox
68 | ```
69 |
70 | By default, the tests are run with the database driver sqlite3. To change this, set the DBTYPE environment variable:
71 |
72 | ```
73 | DBTYPE=postgres trial twistar
74 | DBTYPE=mysql trial twistar
75 | ```
76 |
77 | You'll need a database named "twistar" for each of those tests (or you can change the dbname, user, etc in the `_config.py` file in the tests folder).
78 |
79 | ## Documentation
80 | If you intent on generating API documentation, you will need pydoctor. If you want to generate the user documentation, you will need to install Twisted Lore.
81 |
82 | To generate documentation:
83 |
84 | ```
85 | make docs
86 | ```
87 |
88 | Then open the docs/index.html file in a browser.
89 |
--------------------------------------------------------------------------------
/doc/examples.xhtml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Simple Examples
7 |
8 |
9 |
10 |
Simple Examples
11 |
12 | Since Twistar uses Twisted, most methods and function calls will return a twisted.internet.defer.Deferred
13 | object. You must understand the proper use of these objects before most of this documentation will make sense.
14 |
15 |
16 |
Initialization
17 |
Before using Twistar a connection to the DB must be made. To connect to the database
18 | use Twisted's twisted.enterprise.adapi module and the ConnectionPool
19 | object. The Registry class is used to keep track of that pool. For instance, this is the
20 | method to specify a connection to a MySQL database:
21 |
Twistar does not provide DB creation / migration functionality beyond asynchronously making SQL queries.
39 | There are plenty of utilities in existance to provide these capabilities. If you need to create tables
40 | in an asynchronous manner, you can always execute the creation SQL directly.
41 |
42 |
43 | Objects will assume that the plural version of the object's class name will be the table name. For instance,
44 | if the class name is Person, then the tablename should be People. If it is Chicken, the tablename should be
45 | Chickens. If you want to manually specify a tablename, you can do that with the
46 | TABLENAME class variable (see the
47 | DBObject definition).
48 |
49 |
50 | Column names should have no spaces (with words separated by a _) and should generally be lower case.
51 | All object tables (that don't describe relationships) should have
52 | an auto-incrementing integer column named id.
53 |
54 |
55 |
Defining and Interacting With Objects
56 |
57 | Objects representing rows in a database need only extend the
58 | DBObject class.
59 |
60 |
61 | from twistar.dbobject import DBObject
62 |
63 | class User(DBObject):
64 | pass
65 |
66 |
67 | Assuming that the users table has some VARCHAR fields like first_name and last_name and, say,
68 | and integer field named age (along with the required auto-incrementing integer column named
69 | id), then object properties can be assigned:
70 |
71 |
72 |
73 | def done(user):
74 | print "A user has just been saved with id: %i" % user.id
75 |
76 | u = User()
77 | u.first_name = "John"
78 | u.last_name = "Smith"
79 | u.age = 25
80 | u.save().addCallback(done)
81 |
82 | # The following is equivalent
83 | u = User(first_name="John", last_name="Smith", age=25)
84 | u.save().addCallback(done)
85 |
86 |
87 |
88 | Any additional properties that are set that don't correspond to column names are ignored on the save. For instance,
89 | had a middle_name property been set for the user it would have simply been ignored when the object was saved.
90 |
91 |
92 |
93 | Methods you would traditionally find in an active record style RDBMS are also available, for instance to
94 | test for existance and delete an object instance.
95 |
96 |
97 |
Finding Objects
98 |
To find an object, use the class's find method. It accepts a number of
99 | arguments to group, sort, and limit results. The simplest way to use the method is to get an object
100 | instance by id. For instance, if we wanted to find the user with an id of 1:
101 |
102 |
103 | User.find(1).addCallback(...)
104 |
105 |
106 | Additional constriants can be specified using the where
107 | paramter and others:
108 |
There are many more options, all of which are described on the
113 | DBObject API reference page.
114 |
115 |
116 | One thing to note: if either limit is set to 1 or the query
117 | is by id, then the resulting deferred will return either the
118 | found object or None if not found. Otherwise, an array is returned
119 | containing the found objects.
120 |
121 |
122 |
Class Methods
123 |
124 | Additional class methods (other than find) includes ones for getting an array of all objects
125 | (using the all() method), deleting all instances
126 | (using the deleteAll() method), getting a count of all instances
127 | (using the count() method), and determining whether or not
128 | any particular objects exist (using the exists() method). For instance:
129 |
150 | Twistar also supports validations. Validations describe constraints on an object's
151 | parameters. If those constraints are violated, then an object will not be saved (this
152 | includes both creating and updating). In such a case a special parameter in the object
153 | keeps track of the errors and the messages.
154 |
155 |
156 | As an example, let's say that we want all users to have a first name set and a last
157 | name that has a length between 1 and 256 characters. First, we describe the class
158 | User as above and then add restrictions on it.
159 |
167 | If we look at an object without those parameters, it will be invalid:
168 |
169 |
170 | def vcheck(isValid, object):
171 | if not isValid:
172 | print "Object not valid: %s" % str(u.errors)
173 | u = User()
174 | u.isValid().addCallback(vcheck, u)
175 |
176 |
177 | If we try to save an invalid object, nothing happens:
178 |
179 |
180 | def checkUser(user):
181 | print user.id # this will be None
182 | print user.errors.errorsFor('first_name')
183 | print user.errors.errorsFor('last_name')
184 | print "There were %d errors" % len(user.errors)
185 | User().save().addCallback(checkUser)
186 |
187 |
188 | You can also create your own validity function. It should accept an object as
189 | a parameter and then add errors as necessary. It can return a Deferred
190 | if necessary; the return value from the function will be ignored.
191 |
192 |
193 | def myValidator(user):
194 | if user.first_name != "fred":
195 | user.errors.add('first_name', "must be Fred!")
196 | User.addValidator(myValidator)
197 |
198 | def test(user):
199 | # This will print "First Name must be Fred!"
200 | print user.errors
201 | print user.id # None
202 | User(first_name='not fred').save().addCallback(test)
203 |
204 |
205 | For more information see the Validator and
206 | Errors classes.
207 |
208 |
209 |
DBAPI Column Objects
210 |
211 | The DBAPI specification requires that
212 | implementing modules define classes corresponding to certain column types (like
213 | the class Date). Twistar provides a driver agnostic method for
214 | using those classes using the Registry class:
215 |
216 |
217 | Date = Registry.getDBAPIClass("Date")
218 | bob = User(first_name="Bob", dob=Date(1970, 1, 1))
219 |
220 |
221 | Then, regardless of which DBAPI module you are using, your code will always be using the correct
222 | Date class.
223 |
14 | Twistar is a Python implementation of the active record pattern
15 | (also known as an object-relational mapper or ORM) that uses the Twisted framework's
16 | RDBMS support to provide a non-blocking interface to
17 | relational databases.
18 |
19 |
20 | Since Twistar uses Twisted, most method and function calls will return a twisted.internet.defer.Deferred
21 | object. You must understand the propper use of these objects before most of this documentation will make sense.
22 |
After database initialization and assignment to the Registry.DBPOOL
14 | variable, you can always utilize the twisted.enterprise.adapi.ConnectionPool's
15 | API directly:
16 |
17 |
18 | from twisted.enterprise import adbapi
19 | from twistar.registry import Registry
20 |
21 | def done(result):
22 | print "I just made a table"
23 |
24 | Registry.DBPOOL = adbapi.ConnectionPool('MySQLdb', user="twistar", passwd="apass", db="twistar")
25 | Registry.DBPOOL.runQuery("CREATE DATABASE users (id INT ...").addCallback(done)
26 |
27 |
The alternative, described below, might be much more useful for basic CRUD operations.
28 |
29 |
CRUD Interface
30 |
31 | In the InteractionBase you'll find all of the create/read/update/delete (CRUD)
32 | methods supported by the internal configuration class. To use these methods, use the Registry to get the current configuration
33 | object:
34 |
45 | Each of the CRUD methods gives you more control on the query that is generated. For more information, see the following
46 | methods in InteractionBase:
47 |
48 |
select
49 |
update
50 |
delete
51 |
insert
52 |
insertMany (a more efficient manner of inserting many rows)
Twistar is an Object Relational Mapper (ORM).
12 | It therefore defines ways of interacting with objects and other objects that have relationships with them. The code
13 | here is not necessarily great Twisted code; where typically you would expect to see DeferredLists and
14 | inlineCallbacks there are none to provide clarity to those new to Twisted.
15 |
16 |
17 | More information on relationships can be found in the Relationship class.
18 |
19 |
Has One
20 |
Perhaps the simplest relationship is the Has One relationship. In this example, we will
21 | be using a User object that Has OneAvatar
22 | object.
23 |
24 |
25 | from twistar.dbobject import DBObject
26 | from twistar.registry import Registry
27 |
28 | class User(DBObject):
29 | HASONE = ['avatar']
30 |
31 | class Avatar(DBObject):
32 | pass
33 |
34 | Registry.register(User, Avatar)
35 |
36 |
37 | In this example, we specify that each user can have one avatar. The database at this point should have two tables:
38 |
39 |
A users table with at least a column id.
40 |
A avatars table with at least two columns: id and user_id.
41 |
42 | It is also necessary to register our classes with the registry before we can start utilizing relationships (this is
43 | so the module can find and instantiate them when necessary).
44 |
45 |
46 | For the HASONE class variable we can optionally give a dictionary that provides additional information:
47 |
Another relationship is used when one object "has many" of another. For instance, a User
88 | may have many Pictures.
89 |
90 |
91 | from twistar.dbobject import DBObject
92 | from twistar.registry import Registry
93 |
94 | class User(DBObject):
95 | HASMANY = ['pictures']
96 |
97 | class Picture(DBObject):
98 | pass
99 |
100 | Registry.register(User, Picture)
101 |
102 |
Again, the list assigned to HASMANY can have dictionaries in it just like HASONE that
103 | contain additional configuration options. For this relationship, the database should at this point should have two tables:
104 |
105 |
A users table with at least a column id.
106 |
A pictures table with at least two columns: id and user_id.
107 |
108 | As in the previous example, we can get and set a user's pictures using a special property of user:
109 | pictures.
110 |
111 |
150 | Assuming the DB structure is the same as in the "has many" example, you can now get and set the user that
151 | a particular picture belongs to (using get() and set()
152 | methods on picture.user). Additionally, there is a clear() method that will clear all
153 | objects in a given "belongs to" relationship.
154 |
155 |
Has and Belongs To Many
156 |
157 | In a "has and belongs to many" relationship two objects have a many to many relationship. For instance, users and favorite colors.
158 | A user can have many favorite colors and a favorite color could belong to many users. In this example, there should be three tables —
159 | a users table, a favorite_colors table, and a favorite_colors_users table. The last on the list
160 | is a special table that stores the relationships between the colors and users. It has no primary key, and two columns: one for
161 | user_ids and one for favorite_color_ids. The table's name by convention should be the combination of the other
162 | two table names, joined with an underscore, and arranged alphabetically. The classes would look like:
163 |
177 | You can now get and set the users of favorite colors and the favorite colors of users
178 | (using get() and set()
179 | methods). Additionally, there is a clear() method that will clear all
180 | objects in a given relationship.
181 |
182 |
183 |
Polymorphic Relationships
184 |
185 | With a polymorhpic relationship, a model can belong to more than one other model using a single relationship. For instance, take the example
186 | where two models (say, Boy and Girl) both have many nicknames. Each Nickname
187 | can belong to either a Boy or a Girl. In this example, the tables for boys and girls may have whatever
188 | attributes you would like; the nicknames table, though, needs two columns in addition to an id and a value column. These columns are used to identify the id and
189 | type of the other class. In this case, they will be called nicknameable_id and nicknameable_type.
190 |
232 | You can see that the nicknames for the boy are set via boy.nicknames.set; they are fetched as you would expect via boy.nicknames.get
233 | (with the same form for girls). For each nickname, by calling nickname.nicknameable.get you could end up with either a Boy or a
234 | Girl.
235 |