Merge lp:~cjwatson/storm/sphinx-doc into lp:storm

Proposed by Colin Watson
Status: Merged
Merged at revision: 556
Proposed branch: lp:~cjwatson/storm/sphinx-doc
Merge into: lp:storm
Prerequisite: lp:~cjwatson/storm/doctest-cleanup
Diff against target: 2753 lines (+1268/-893)
14 files modified
.bzrignore (+1/-0)
MANIFEST.in (+4/-1)
NEWS (+5/-0)
README (+1/-1)
setup.py (+8/-1)
storm/docs/Makefile (+19/-0)
storm/docs/api.rst (+150/-0)
storm/docs/conf.py (+197/-0)
storm/docs/index.rst (+25/-0)
storm/docs/infoheritance.rst (+230/-240)
storm/docs/tutorial.rst (+566/-604)
storm/docs/zope.rst (+46/-41)
storm/tests/__init__.py (+7/-5)
tox.ini (+9/-0)
To merge this branch: bzr merge lp:~cjwatson/storm/sphinx-doc
Reviewer Review Type Date Requested Status
Ioana Lasc (community) Approve
Storm Developers Pending
Review via email: mp+384532@code.launchpad.net

Commit message

Add Sphinx documentation.

Description of the change

This is loosely inspired by similar work done by Andreas Runfalk in https://github.com/runfalk/storm-legacy, although I made these changes independently (mainly because that fork also switches to pytest, which I didn't want to do).

I've converted the existing doctests to reStructuredText and integrated them as narrative documentation. They're still run as doctests.

To post a comment you must log in.
Revision history for this message
Ioana Lasc (ilasc) :
review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file '.bzrignore'
2--- .bzrignore 2019-09-29 14:05:31 +0000
3+++ .bzrignore 2020-05-26 10:31:46 +0000
4@@ -13,6 +13,7 @@
5 debian/python-storm.prerm.debhelper
6 debian/python-storm.substvars
7 apidoc
8+storm/docs/_build
9 .tox
10 _trial_temp
11 *.egg
12
13=== modified file 'MANIFEST.in'
14--- MANIFEST.in 2019-11-21 11:49:06 +0000
15+++ MANIFEST.in 2020-05-26 10:31:46 +0000
16@@ -1,5 +1,8 @@
17-recursive-include storm *.py *.c *.zcml *.txt
18+recursive-include storm *.py *.c *.zcml *.rst
19
20 include MANIFEST.in LICENSE README TODO NEWS Makefile dev/test setup.cfg tox.ini
21
22+include storm/docs/Makefile
23+prune storm/docs/_build
24+
25 prune db
26
27=== modified file 'NEWS'
28--- NEWS 2020-05-19 09:02:51 +0000
29+++ NEWS 2020-05-26 10:31:46 +0000
30@@ -1,6 +1,11 @@
31 0.24
32 ====
33
34+Improvements
35+------------
36+
37+- Add Sphinx documentation.
38+
39 Bug fixes
40 ---------
41
42
43=== modified file 'README'
44--- README 2019-11-21 11:29:33 +0000
45+++ README 2020-05-26 10:31:46 +0000
46@@ -59,7 +59,7 @@
47 License
48 =======
49
50-Copyright (C) 2006-2009 Canonical, Ltd. All contributions must have
51+Copyright (C) 2006-2020 Canonical, Ltd. All contributions must have
52 copyright assigned to Canonical.
53
54 This library is free software; you can redistribute it and/or
55
56=== modified file 'setup.py'
57--- setup.py 2020-04-25 15:52:01 +0000
58+++ setup.py 2020-05-26 10:31:46 +0000
59@@ -86,5 +86,12 @@
60 install_requires=["six"],
61 test_suite="storm.tests.find_tests",
62 tests_require=tests_require,
63- extras_require={"test": tests_require},
64+ extras_require={
65+ "doc": [
66+ "fixtures", # so that storm.testing can be imported
67+ "sphinx",
68+ "sphinx-epytext",
69+ ],
70+ "test": tests_require,
71+ },
72 )
73
74=== added directory 'storm/docs'
75=== added file 'storm/docs/Makefile'
76--- storm/docs/Makefile 1970-01-01 00:00:00 +0000
77+++ storm/docs/Makefile 2020-05-26 10:31:46 +0000
78@@ -0,0 +1,19 @@
79+# Minimal makefile for Sphinx documentation
80+#
81+
82+# You can set these variables from the command line.
83+SPHINXOPTS =
84+SPHINXBUILD = sphinx-build
85+SOURCEDIR = .
86+BUILDDIR = _build
87+
88+# Put it first so that "make" without argument is like "make help".
89+help:
90+ @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
91+
92+.PHONY: help Makefile
93+
94+# Catch-all target: route all unknown targets to Sphinx using the new
95+# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
96+%: Makefile
97+ @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
98
99=== added file 'storm/docs/__init__.py'
100=== added file 'storm/docs/api.rst'
101--- storm/docs/api.rst 1970-01-01 00:00:00 +0000
102+++ storm/docs/api.rst 2020-05-26 10:31:46 +0000
103@@ -0,0 +1,150 @@
104+API
105+===
106+.. contents:: :local:
107+
108+
109+Locals
110+------
111+
112+The following names are re-exported from ``storm.locals`` for convenience:
113+
114+* :py:class:`storm.base.Storm`
115+* :py:func:`storm.database.create_database`
116+* :py:class:`storm.exceptions.StormError`
117+* :py:class:`storm.expr.And`
118+* :py:class:`storm.expr.Asc`
119+* :py:class:`storm.expr.Count`
120+* :py:class:`storm.expr.Delete`
121+* :py:class:`storm.expr.Desc`
122+* :py:class:`storm.expr.In`
123+* :py:class:`storm.expr.Insert`
124+* :py:class:`storm.expr.Join`
125+* :py:class:`storm.expr.Like`
126+* :py:class:`storm.expr.Max`
127+* :py:class:`storm.expr.Min`
128+* :py:class:`storm.expr.Not`
129+* :py:class:`storm.expr.Or`
130+* :py:class:`storm.expr.SQL`
131+* :py:class:`storm.expr.Select`
132+* :py:class:`storm.expr.Update`
133+* :py:class:`storm.info.ClassAlias`
134+* :py:class:`storm.properties.Bool`
135+* :py:class:`storm.properties.Bytes`
136+* :py:class:`storm.properties.Date`
137+* :py:class:`storm.properties.DateTime`
138+* :py:class:`storm.properties.Decimal`
139+* :py:class:`storm.properties.Enum`
140+* :py:class:`storm.properties.Float`
141+* :py:class:`storm.properties.Int`
142+* :py:class:`storm.properties.JSON`
143+* :py:class:`storm.properties.List`
144+* :py:class:`storm.properties.Pickle`
145+* :py:class:`storm.properties.Time`
146+* :py:class:`storm.properties.TimeDelta`
147+* :py:class:`storm.properties.UUID`
148+* :py:class:`storm.properties.Unicode`
149+* :py:class:`storm.references.Proxy`
150+* :py:class:`storm.references.Reference`
151+* :py:class:`storm.references.ReferenceSet`
152+* :py:data:`storm.store.AutoReload`
153+* :py:class:`storm.store.Store`
154+* :py:class:`storm.xid.Xid`
155+
156+
157+
158+Store
159+-----
160+.. automodule:: storm.store
161+.. autoclass:: storm.store.ResultSet
162+.. autoclass:: storm.store.TableSet
163+.. autodata:: storm.store.AutoReload
164+ :annotation:
165+
166+Defining tables and columns
167+---------------------------
168+
169+Base
170+~~~~
171+.. automodule:: storm.base
172+
173+Properties
174+~~~~~~~~~~
175+.. automodule:: storm.properties
176+
177+References
178+~~~~~~~~~~
179+.. automodule:: storm.references
180+
181+Variables
182+~~~~~~~~~
183+.. automodule:: storm.variables
184+
185+SQLObject emulation
186+~~~~~~~~~~~~~~~~~~~
187+.. automodule:: storm.sqlobject
188+
189+
190+Expressions
191+-----------
192+.. automodule:: storm.expr
193+
194+
195+Databases
196+---------
197+.. automodule:: storm.database
198+
199+PostgreSQL
200+~~~~~~~~~~
201+.. automodule:: storm.databases.postgres
202+
203+SQLite
204+~~~~~~
205+.. automodule:: storm.databases.sqlite
206+
207+Transaction identifiers
208+~~~~~~~~~~~~~~~~~~~~~~~
209+.. automodule:: storm.xid
210+
211+
212+Hooks and events
213+----------------
214+
215+Event
216+~~~~~
217+.. automodule:: storm.event
218+
219+Tracer
220+~~~~~~
221+.. automodule:: storm.tracer
222+
223+
224+Miscellaneous
225+-------------
226+
227+Cache
228+~~~~~
229+.. automodule:: storm.cache
230+
231+Exceptions
232+~~~~~~~~~~
233+.. automodule:: storm.exceptions
234+
235+Info
236+~~~~~
237+.. automodule:: storm.info
238+
239+Testing
240+~~~~~~~
241+.. automodule:: storm.testing
242+
243+Timezone
244+~~~~~~~~
245+.. automodule:: storm.tz
246+
247+URIs
248+~~~~
249+.. automodule:: storm.uri
250+
251+WSGI
252+~~~~
253+.. automodule:: storm.wsgi
254
255=== added file 'storm/docs/conf.py'
256--- storm/docs/conf.py 1970-01-01 00:00:00 +0000
257+++ storm/docs/conf.py 2020-05-26 10:31:46 +0000
258@@ -0,0 +1,197 @@
259+# -*- coding: utf-8 -*-
260+#
261+# Configuration file for the Sphinx documentation builder.
262+#
263+# This file does only contain a selection of the most common options. For a
264+# full list see the documentation:
265+# http://www.sphinx-doc.org/en/master/config
266+
267+# -- Path setup --------------------------------------------------------------
268+
269+# If extensions (or modules to document with autodoc) are in another directory,
270+# add these directories to sys.path here. If the directory is relative to the
271+# documentation root, use os.path.abspath to make it absolute, like shown here.
272+
273+import os
274+import sys
275+sys.path.insert(0, os.path.abspath('..'))
276+
277+# Import and document the pure-Python versions of things.
278+os.environ['STORM_CEXTENSIONS'] = '0'
279+
280+
281+# -- Project information -----------------------------------------------------
282+
283+project = 'Storm'
284+copyright = '2006-2020, Canonical Ltd.'
285+author = 'Gustavo Niemeyer'
286+
287+# The short X.Y version
288+version = ''
289+# The full version, including alpha/beta/rc tags
290+release = ''
291+
292+
293+# -- General configuration ---------------------------------------------------
294+
295+# If your documentation needs a minimal Sphinx version, state it here.
296+#
297+# needs_sphinx = '1.0'
298+
299+# Fix missing support for @ivar and @cvar.
300+from sphinx_epytext.process_docstring import FIELDS
301+FIELDS.append("cvar")
302+FIELDS.append("ivar")
303+
304+# Add any Sphinx extension module names here, as strings. They can be
305+# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
306+# ones.
307+extensions = [
308+ 'sphinx.ext.autodoc',
309+ 'sphinx.ext.doctest',
310+ 'sphinx.ext.intersphinx',
311+ 'sphinx_epytext',
312+]
313+
314+# Add any paths that contain templates here, relative to this directory.
315+# templates_path = ['_templates']
316+
317+# The suffix(es) of source filenames.
318+# You can specify multiple suffix as a list of string:
319+#
320+# source_suffix = ['.rst', '.md']
321+source_suffix = '.rst'
322+
323+# The master toctree document.
324+master_doc = 'index'
325+
326+# The language for content autogenerated by Sphinx. Refer to documentation
327+# for a list of supported languages.
328+#
329+# This is also used if you do content translation via gettext catalogs.
330+# Usually you set "language" from the command line for these cases.
331+language = None
332+
333+# List of patterns, relative to source directory, that match files and
334+# directories to ignore when looking for source files.
335+# This pattern also affects html_static_path and html_extra_path.
336+exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
337+
338+# The name of the Pygments (syntax highlighting) style to use.
339+pygments_style = None
340+
341+
342+# -- Options for HTML output -------------------------------------------------
343+
344+# The theme to use for HTML and HTML Help pages. See the documentation for
345+# a list of builtin themes.
346+#
347+html_theme = 'alabaster'
348+
349+# Theme options are theme-specific and customize the look and feel of a theme
350+# further. For a list of options available for each theme, see the
351+# documentation.
352+#
353+# html_theme_options = {}
354+
355+# Add any paths that contain custom static files (such as style sheets) here,
356+# relative to this directory. They are copied after the builtin static files,
357+# so a file named "default.css" will overwrite the builtin "default.css".
358+# html_static_path = ['_static']
359+
360+# Custom sidebar templates, must be a dictionary that maps document names
361+# to template names.
362+#
363+# The default sidebars (for documents that don't match any pattern) are
364+# defined by theme itself. Builtin themes are using these templates by
365+# default: ``['localtoc.html', 'relations.html', 'sourcelink.html',
366+# 'searchbox.html']``.
367+#
368+# html_sidebars = {}
369+
370+
371+# -- Options for HTMLHelp output ---------------------------------------------
372+
373+# Output file base name for HTML help builder.
374+htmlhelp_basename = 'Stormdoc'
375+
376+
377+# -- Options for LaTeX output ------------------------------------------------
378+
379+latex_elements = {
380+ # The paper size ('letterpaper' or 'a4paper').
381+ #
382+ # 'papersize': 'letterpaper',
383+
384+ # The font size ('10pt', '11pt' or '12pt').
385+ #
386+ # 'pointsize': '10pt',
387+
388+ # Additional stuff for the LaTeX preamble.
389+ #
390+ # 'preamble': '',
391+
392+ # Latex figure (float) alignment
393+ #
394+ # 'figure_align': 'htbp',
395+}
396+
397+# Grouping the document tree into LaTeX files. List of tuples
398+# (source start file, target name, title,
399+# author, documentclass [howto, manual, or own class]).
400+latex_documents = [
401+ (master_doc, 'Storm.tex', 'Storm Documentation',
402+ 'Gustavo Niemeyer', 'manual'),
403+]
404+
405+
406+# -- Options for manual page output ------------------------------------------
407+
408+# One entry per manual page. List of tuples
409+# (source start file, name, description, authors, manual section).
410+man_pages = [
411+ (master_doc, 'storm', 'Storm Documentation',
412+ [author], 1)
413+]
414+
415+
416+# -- Options for Texinfo output ----------------------------------------------
417+
418+# Grouping the document tree into Texinfo files. List of tuples
419+# (source start file, target name, title, author,
420+# dir menu entry, description, category)
421+texinfo_documents = [
422+ (master_doc, 'Storm', 'Storm Documentation',
423+ author, 'Storm', 'One line description of project.',
424+ 'Miscellaneous'),
425+]
426+
427+
428+# -- Options for Epub output -------------------------------------------------
429+
430+# Bibliographic Dublin Core info.
431+epub_title = project
432+
433+# The unique identifier of the text. This can be a ISBN number
434+# or the project homepage.
435+#
436+# epub_identifier = ''
437+
438+# A unique identification for the text.
439+#
440+# epub_uid = ''
441+
442+# A list of files that should not be packed into the epub file.
443+epub_exclude_files = ['search.html']
444+
445+intersphinx_mapping = {'https://docs.python.org/3': None}
446+
447+# Sphinx 1.8+ prefers this to `autodoc_default_flags`. It's documented that
448+# either True or None mean the same thing as just setting the flag, but
449+# only None works in 1.8 (True works in 2.0)
450+autodoc_default_options = {
451+ 'members': None,
452+ 'show-inheritance': None,
453+}
454+autodoc_member_order = 'bysource'
455+autoclass_content = 'both'
456
457=== added file 'storm/docs/index.rst'
458--- storm/docs/index.rst 1970-01-01 00:00:00 +0000
459+++ storm/docs/index.rst 2020-05-26 10:31:46 +0000
460@@ -0,0 +1,25 @@
461+.. Storm documentation master file, created by
462+ sphinx-quickstart on Sat May 23 01:56:39 2020.
463+ You can adapt this file completely to your liking, but it should at least
464+ contain the root `toctree` directive.
465+
466+Welcome to Storm's documentation!
467+=================================
468+
469+.. toctree::
470+ :maxdepth: 2
471+ :caption: Contents:
472+
473+ tutorial
474+ infoheritance
475+ zope
476+ api
477+
478+
479+
480+Indices and tables
481+==================
482+
483+* :ref:`genindex`
484+* :ref:`modindex`
485+* :ref:`search`
486
487=== renamed file 'storm/tests/infoheritance.txt' => 'storm/docs/infoheritance.rst'
488--- storm/tests/infoheritance.txt 2020-05-26 10:31:46 +0000
489+++ storm/docs/infoheritance.rst 2020-05-26 10:31:46 +0000
490@@ -1,11 +1,5 @@
491-This Storm document is included in Storm's source code at
492-`storm/tests/infoheritance.txt` so that it can be tested and be kept
493-up-to-date.
494-
495-<<TableOfContents>>
496-
497-
498-==== Introduction ====
499+Infoheritance
500+=============
501
502 Storm doesn't support classes that have columns in multiple tables. This
503 makes using inheritance rather difficult. The infoheritance pattern described
504@@ -13,40 +7,43 @@
505 the problems Storm has with multi-table classes.
506
507
508-==== Defining a sample model ====
509+Defining a sample model
510+-----------------------
511
512 Let's consider an inheritance hierarchy to migrate to Storm.
513
514-{{{#!python
515-class Person(object):
516-
517- def __init__(self, name):
518- self.name = name
519-
520-
521-class SecretAgent(Person):
522-
523- def __init__(self, name, passcode):
524- super(SecretAgent, self).__init__(name)
525- self.passcode = passcode
526-
527-
528-class Teacher(Person):
529-
530- def __init__(self, name, school):
531- super(Employee, self).__init__(name):
532- self.school = school
533-}}}
534-
535-We want to use three tables to store data for these objects: `person`,
536-`secret_agent` and `teacher`. We can't simply convert instance attributes to
537-Storm properties and add `__storm_table__` definitions because a single object
538-may not have columns that come from more than one table. We can't have
539-`Teacher` getting it's `name` column from the `person` table and it's `school`
540-column from the `teacher` table, for example.
541-
542-
543-==== The infoheritance pattern ====
544+.. code-block:: python
545+
546+ class Person(object):
547+
548+ def __init__(self, name):
549+ self.name = name
550+
551+
552+ class SecretAgent(Person):
553+
554+ def __init__(self, name, passcode):
555+ super(SecretAgent, self).__init__(name)
556+ self.passcode = passcode
557+
558+
559+ class Teacher(Person):
560+
561+ def __init__(self, name, school):
562+ super(Employee, self).__init__(name):
563+ self.school = school
564+
565+We want to use three tables to store data for these objects: ``person``,
566+``secret_agent`` and ``teacher``. We can't simply convert instance
567+attributes to Storm properties and add ``__storm_table__`` definitions
568+because a single object may not have columns that come from more than one
569+table. We can't have ``Teacher`` getting its ``name`` column from the
570+``person`` table and its ``school`` column from the ``teacher`` table, for
571+example.
572+
573+
574+The infoheritance pattern
575+-------------------------
576
577 The infoheritance pattern uses composition instead of inheritance to work
578 around the multiple table limitation. A base Storm class is used to represent
579@@ -55,230 +52,223 @@
580 provides the additional data and behaviour you'd normally implement in a
581 subclass. Following is the design from above converted to use the pattern.
582
583-{{{#!python
584->>> from storm.locals import Storm, Store, Int, Unicode, Reference
585-
586->>> person_info_types = {}
587-
588->>> def register_person_info_type(info_type, info_class):
589-... existing_info_class = person_info_types.get(info_type)
590-... if existing_info_class is not None:
591-... raise RuntimeError("%r has the same info_type of %r" %
592-... (info_class, existing_info_class))
593-... person_info_types[info_type] = info_class
594-... info_class.info_type = info_type
595-
596-
597->>> class Person(Storm):
598-...
599-... __storm_table__ = "person"
600-...
601-... id = Int(allow_none=False, primary=True)
602-... name = Unicode(allow_none=False)
603-... info_type = Int(allow_none=False)
604-... _info = None
605-...
606-... def __init__(self, store, name, info_class, **kwargs):
607-... self.name = name
608-... self.info_type = info_class.info_type
609-... store.add(self)
610-... self._info = info_class(self, **kwargs)
611-...
612-... @property
613-... def info(self):
614-... if self._info is not None:
615-... return self._info
616-... assert self.id is not None
617-... info_class = person_info_types[self.info_type]
618-... if not hasattr(info_class, "__storm_table__"):
619-... info = info_class.__new__(info_class)
620-... info.person = self
621-... else:
622-... info = Store.of(self).get(info_class, self.id)
623-... self._info = info
624-... return info
625-
626-
627->>> class PersonInfo(object):
628-...
629-... def __init__(self, person):
630-... self.person = person
631-
632-
633->>> class StoredPersonInfo(PersonInfo):
634-...
635-... person_id = Int(allow_none=False, primary=True)
636-... person = Reference(person_id, Person.id)
637-
638-
639->>> class SecretAgent(StoredPersonInfo):
640-...
641-... __storm_table__ = "secret_agent"
642-...
643-... passcode = Unicode(allow_none=False)
644-...
645-... def __init__(self, person, passcode=None):
646-... super(SecretAgent, self).__init__(person)
647-... self.passcode = passcode
648-
649-
650->>> class Teacher(StoredPersonInfo):
651-...
652-... __storm_table__ = "teacher"
653-...
654-... school = Unicode(allow_none=False)
655-...
656-... def __init__(self, person, school=None):
657-... super(Teacher, self).__init__(person)
658-... self.school = school
659->>>
660-}}}
661-
662-The pattern works by having a base class, `Person`, keep a reference to an
663-info class, `PersonInfo`. Info classes need to be registered so that `Person`
664-can discover them and load them when necessary. Note that info types have the
665-same ID as their parent object. This isn't strictly necessary, but it makes
666-certain things easy, such as being able to look up info objects directly by ID
667-when given a person object. `Person` objects are required to be in a store to
668-ensure that an ID is available and can used by the info class.
669-
670-
671-==== Registering info classes ====
672+.. doctest::
673+
674+ >>> from storm.locals import Storm, Store, Int, Unicode, Reference
675+
676+ >>> person_info_types = {}
677+
678+ >>> def register_person_info_type(info_type, info_class):
679+ ... existing_info_class = person_info_types.get(info_type)
680+ ... if existing_info_class is not None:
681+ ... raise RuntimeError("%r has the same info_type of %r" %
682+ ... (info_class, existing_info_class))
683+ ... person_info_types[info_type] = info_class
684+ ... info_class.info_type = info_type
685+
686+
687+ >>> class Person(Storm):
688+ ...
689+ ... __storm_table__ = "person"
690+ ...
691+ ... id = Int(allow_none=False, primary=True)
692+ ... name = Unicode(allow_none=False)
693+ ... info_type = Int(allow_none=False)
694+ ... _info = None
695+ ...
696+ ... def __init__(self, store, name, info_class, **kwargs):
697+ ... self.name = name
698+ ... self.info_type = info_class.info_type
699+ ... store.add(self)
700+ ... self._info = info_class(self, **kwargs)
701+ ...
702+ ... @property
703+ ... def info(self):
704+ ... if self._info is not None:
705+ ... return self._info
706+ ... assert self.id is not None
707+ ... info_class = person_info_types[self.info_type]
708+ ... if not hasattr(info_class, "__storm_table__"):
709+ ... info = info_class.__new__(info_class)
710+ ... info.person = self
711+ ... else:
712+ ... info = Store.of(self).get(info_class, self.id)
713+ ... self._info = info
714+ ... return info
715+
716+
717+ >>> class PersonInfo(object):
718+ ...
719+ ... def __init__(self, person):
720+ ... self.person = person
721+
722+
723+ >>> class StoredPersonInfo(PersonInfo):
724+ ...
725+ ... person_id = Int(allow_none=False, primary=True)
726+ ... person = Reference(person_id, Person.id)
727+
728+
729+ >>> class SecretAgent(StoredPersonInfo):
730+ ...
731+ ... __storm_table__ = "secret_agent"
732+ ...
733+ ... passcode = Unicode(allow_none=False)
734+ ...
735+ ... def __init__(self, person, passcode=None):
736+ ... super(SecretAgent, self).__init__(person)
737+ ... self.passcode = passcode
738+
739+
740+ >>> class Teacher(StoredPersonInfo):
741+ ...
742+ ... __storm_table__ = "teacher"
743+ ...
744+ ... school = Unicode(allow_none=False)
745+ ...
746+ ... def __init__(self, person, school=None):
747+ ... super(Teacher, self).__init__(person)
748+ ... self.school = school
749+
750+The pattern works by having a base class, ``Person``, keep a reference to an
751+info class, ``PersonInfo``. Info classes need to be registered so that
752+``Person`` can discover them and load them when necessary. Note that info
753+types have the same ID as their parent object. This isn't strictly
754+necessary, but it makes certain things easy, such as being able to look up
755+info objects directly by ID when given a person object. ``Person`` objects
756+are required to be in a store to ensure that an ID is available and can used
757+by the info class.
758+
759+
760+Registering info classes
761+------------------------
762
763 Let's register our info classes. Each class must be registered with a unique
764 info type key. This key is stored in the database, so be sure to use a stable
765 value.
766
767-{{{#!python
768->>> register_person_info_type(1, SecretAgent)
769->>> register_person_info_type(2, Teacher)
770->>>
771-}}}
772+.. doctest::
773+
774+ >>> register_person_info_type(1, SecretAgent)
775+ >>> register_person_info_type(2, Teacher)
776
777 Let's create a database to store person objects before we continue.
778
779-{{{#!python
780->>> from storm.locals import create_database
781-
782->>> database = create_database("sqlite:")
783->>> store = Store(database)
784->>> result = store.execute("""
785-... CREATE TABLE person (
786-... id INTEGER PRIMARY KEY,
787-... info_type INTEGER NOT NULL,
788-... name TEXT NOT NULL)
789-... """)
790->>> result = store.execute("""
791-... CREATE TABLE secret_agent (
792-... person_id INTEGER PRIMARY KEY,
793-... passcode TEXT NOT NULL)
794-... """)
795->>> result = store.execute("""
796-... CREATE TABLE teacher (
797-... person_id INTEGER PRIMARY KEY,
798-... school TEXT NOT NULL)
799-... """)
800->>>
801-}}}
802-
803-
804-==== Creating info classes ====
805+.. doctest::
806+
807+ >>> from storm.locals import create_database
808+
809+ >>> database = create_database("sqlite:")
810+ >>> store = Store(database)
811+ >>> result = store.execute("""
812+ ... CREATE TABLE person (
813+ ... id INTEGER PRIMARY KEY,
814+ ... info_type INTEGER NOT NULL,
815+ ... name TEXT NOT NULL)
816+ ... """)
817+ >>> result = store.execute("""
818+ ... CREATE TABLE secret_agent (
819+ ... person_id INTEGER PRIMARY KEY,
820+ ... passcode TEXT NOT NULL)
821+ ... """)
822+ >>> result = store.execute("""
823+ ... CREATE TABLE teacher (
824+ ... person_id INTEGER PRIMARY KEY,
825+ ... school TEXT NOT NULL)
826+ ... """)
827+
828+
829+Creating info classes
830+---------------------
831
832 We can easily create person objects now.
833
834-{{{#!python
835->>> secret_agent = Person(store, u"Dick Tracy",
836-... SecretAgent, passcode=u"secret!")
837->>> teacher = Person(store, u"Mrs. Cohen",
838-... Teacher, school=u"Cameron Elementary School")
839->>> store.commit()
840->>>
841-}}}
842+.. doctest::
843+
844+ >>> secret_agent = Person(store, u"Dick Tracy",
845+ ... SecretAgent, passcode=u"secret!")
846+ >>> teacher = Person(store, u"Mrs. Cohen",
847+ ... Teacher, school=u"Cameron Elementary School")
848+ >>> store.commit()
849
850 And we can easily find them again.
851
852-{{{#!python
853->>> del secret_agent
854->>> del teacher
855->>> store.rollback()
856-
857->>> [type(person.info) for person in store.find(Person).order_by(Person.name)]
858-[<class '...SecretAgent'>, <class '...Teacher'>]
859->>>
860-}}}
861-
862-
863-==== Retrieving info classes ====
864-
865-Now that we have our basic hierarchy in place we're going to want to retrieve
866-objects by info type. Let's implement a function to make finding `Person`s
867-easier.
868-
869-{{{#!python
870->>> def get_persons(store, info_classes=None):
871-... where = []
872-... if info_classes:
873-... info_types = [info_class.info_type for info_class in info_classes]
874-... where = [Person.info_type.is_in(info_types)]
875-... result = store.find(Person, *where)
876-... result.order_by(Person.name)
877-... return result
878-
879->>> secret_agent = get_persons(store, info_classes=[SecretAgent]).one()
880->>> print(secret_agent.name)
881-Dick Tracy
882->>> print(secret_agent.info.passcode)
883-secret!
884-
885->>> teacher = get_persons(store, info_classes=[Teacher]).one()
886->>> print(teacher.name)
887-Mrs. Cohen
888->>> print(teacher.info.school)
889-Cameron Elementary School
890-
891->>>
892-}}}
893-
894-Great, we can easily find different kinds of `Person`s.
895-
896-
897-==== In-memory info objects ====
898+.. doctest::
899+
900+ >>> del secret_agent
901+ >>> del teacher
902+ >>> store.rollback()
903+
904+ >>> [type(person.info)
905+ ... for person in store.find(Person).order_by(Person.name)]
906+ [<class '...SecretAgent'>, <class '...Teacher'>]
907+
908+
909+Retrieving info classes
910+-----------------------
911+
912+Now that we have our basic hierarchy in place we're going to want to
913+retrieve objects by info type. Let's implement a function to make finding
914+``Person``\ s easier.
915+
916+.. doctest::
917+
918+ >>> def get_persons(store, info_classes=None):
919+ ... where = []
920+ ... if info_classes:
921+ ... info_types = [
922+ ... info_class.info_type for info_class in info_classes]
923+ ... where = [Person.info_type.is_in(info_types)]
924+ ... result = store.find(Person, *where)
925+ ... result.order_by(Person.name)
926+ ... return result
927+
928+ >>> secret_agent = get_persons(store, info_classes=[SecretAgent]).one()
929+ >>> print(secret_agent.name)
930+ Dick Tracy
931+ >>> print(secret_agent.info.passcode)
932+ secret!
933+
934+ >>> teacher = get_persons(store, info_classes=[Teacher]).one()
935+ >>> print(teacher.name)
936+ Mrs. Cohen
937+ >>> print(teacher.info.school)
938+ Cameron Elementary School
939+
940+Great, we can easily find different kinds of ``Person``\ s.
941+
942+
943+In-memory info objects
944+----------------------
945
946 This design also allows for in-memory info objects. Let's add one to our
947 hierarchy.
948
949-{{{#!python
950->>> class Ghost(PersonInfo):
951-...
952-... friendly = True
953-
954->>> register_person_info_type(3, Ghost)
955->>>
956-}}}
957+.. doctest::
958+
959+ >>> class Ghost(PersonInfo):
960+ ...
961+ ... friendly = True
962+
963+ >>> register_person_info_type(3, Ghost)
964
965 We create and load in-memory objects the same way we do stored ones.
966
967-{{{#!python
968->>> ghost = Person(store, u"Casper", Ghost)
969->>> store.commit()
970->>> del ghost
971->>> store.rollback()
972-
973->>> ghost = get_persons(store, info_classes=[Ghost]).one()
974->>> print(ghost.name)
975-Casper
976->>> print(ghost.info.friendly)
977-True
978-
979->>>
980-}}}
981+.. doctest::
982+
983+ >>> ghost = Person(store, u"Casper", Ghost)
984+ >>> store.commit()
985+ >>> del ghost
986+ >>> store.rollback()
987+
988+ >>> ghost = get_persons(store, info_classes=[Ghost]).one()
989+ >>> print(ghost.name)
990+ Casper
991+ >>> print(ghost.info.friendly)
992+ True
993
994 This pattern is very handy when using Storm with code that would naturally be
995 implemented using inheritance.
996
997-
998-==== Clean up ====
999-
1000+..
1001 >>> Person._storm_property_registry.clear()
1002-
1003-## vim:ts=4:sw=4:et:ft=moin1_5
1004
1005=== renamed file 'storm/tests/tutorial.txt' => 'storm/docs/tutorial.rst'
1006--- storm/tests/tutorial.txt 2020-05-26 10:31:46 +0000
1007+++ storm/docs/tutorial.rst 2020-05-26 10:31:46 +0000
1008@@ -1,154 +1,145 @@
1009-
1010-[[TableOfContents]]
1011-
1012-==== It runs! ====
1013-
1014-This Storm tutorial is included in the source code at
1015-storm/tests/tutorial.txt, so that it may be tested and stay
1016-up to date.
1017-
1018-
1019-==== Importing ====
1020+Tutorial
1021+========
1022+
1023+Importing
1024+---------
1025
1026 Let's start by importing some names into the namespace.
1027
1028-{{{#!python
1029->>> from storm.locals import *
1030->>>
1031-}}}
1032-
1033-==== Basic definition ====
1034+.. doctest::
1035+
1036+ >>> from storm.locals import *
1037+
1038+Basic definition
1039+----------------
1040
1041 Now we define a type with some properties describing the information
1042 we're about to map.
1043
1044-{{{#!python
1045->>> class Person(object):
1046-... __storm_table__ = "person"
1047-... id = Int(primary=True)
1048-... name = Unicode()
1049+.. doctest::
1050
1051-}}}
1052+ >>> class Person(object):
1053+ ... __storm_table__ = "person"
1054+ ... id = Int(primary=True)
1055+ ... name = Unicode()
1056
1057 Notice that this has no Storm-defined base class or constructor.
1058
1059-==== Creating a database and the store ====
1060+Creating a database and the store
1061+---------------------------------
1062
1063 We still don't have anyone to talk to, so let's define an in-memory
1064 SQLite database to play with, and a store using that database.
1065
1066-{{{#!python
1067->>> database = create_database("sqlite:")
1068->>> store = Store(database)
1069->>>
1070-}}}
1071+.. doctest::
1072+
1073+ >>> database = create_database("sqlite:")
1074+ >>> store = Store(database)
1075
1076 Two databases are supported at the moment: SQLite and PostgreSQL.
1077-The parameter passed to `create_database()` is an URI, as follows:
1078-
1079-{{{
1080-database = create_database("scheme://username:password@hostname:port/database_name")
1081-}}}
1082-
1083-The scheme may be "sqlite" or "postgres".
1084+The parameter passed to :py:func:`~storm.database.create_database` is an
1085+URI, as follows:
1086+
1087+.. code-block:: python
1088+
1089+ # database = create_database(
1090+ # "scheme://username:password@hostname:port/database_name")
1091+
1092+The ``scheme`` may be ``sqlite`` or ``postgres``.
1093
1094 Now we have to create the table that will actually hold the data
1095 for our class.
1096
1097-{{{#!python
1098->>> store.execute("CREATE TABLE person "
1099-... "(id INTEGER PRIMARY KEY, name VARCHAR)")
1100-<storm.databases.sqlite.SQLiteResult object at 0x...>
1101+.. doctest::
1102
1103-}}}
1104+ >>> store.execute("CREATE TABLE person "
1105+ ... "(id INTEGER PRIMARY KEY, name VARCHAR)")
1106+ <storm.databases.sqlite.SQLiteResult object at 0x...>
1107
1108 We got a result back, but we don't care about it for now. We could also
1109-use `noresult=True` to avoid the result entirely.
1110+use ``noresult=True`` to avoid the result entirely.
1111
1112-==== Creating an object ====
1113+Creating an object
1114+------------------
1115
1116 Let's create an object of the defined class.
1117
1118-{{{#!python
1119->>> joe = Person()
1120->>> joe.name = u"Joe Johnes"
1121-
1122->>> print(joe.id)
1123-None
1124->>> print(joe.name)
1125-Joe Johnes
1126-
1127-}}}
1128+.. doctest::
1129+
1130+ >>> joe = Person()
1131+ >>> joe.name = u"Joe Johnes"
1132+
1133+ >>> print(joe.id)
1134+ None
1135+ >>> print(joe.name)
1136+ Joe Johnes
1137
1138 So far this object has no connection to a database. Let's add it to the
1139 store we've created above.
1140
1141-{{{#!python
1142->>> store.add(joe)
1143-<...Person object at 0x...>
1144-
1145->>> print(joe.id)
1146-None
1147->>> print(joe.name)
1148-Joe Johnes
1149-
1150-}}}
1151+.. doctest::
1152+
1153+ >>> store.add(joe)
1154+ <...Person object at 0x...>
1155+
1156+ >>> print(joe.id)
1157+ None
1158+ >>> print(joe.name)
1159+ Joe Johnes
1160
1161 Notice that the object wasn't changed, even after being added to the
1162 store. That's because it wasn't flushed yet.
1163
1164-==== The store of an object ====
1165+The store of an object
1166+----------------------
1167
1168-Once an object is added to a store, or retrieved from a store, it's
1169+Once an object is added to a store, or retrieved from a store, its
1170 relation to that store is known. We can easily verify which store
1171 an object is bound.
1172
1173-{{{
1174->>> Store.of(joe) is store
1175-True
1176-
1177->>> Store.of(Person()) is None
1178-True
1179-
1180-}}}
1181-
1182-==== Finding an object ====
1183+.. doctest::
1184+
1185+ >>> Store.of(joe) is store
1186+ True
1187+
1188+ >>> Store.of(Person()) is None
1189+ True
1190+
1191+Finding an object
1192+-----------------
1193
1194 Now, what would happen if we actually asked the store to give us
1195-the person named ''Joe Johnes''?
1196-
1197-{{{#!python
1198->>> person = store.find(Person, Person.name == u"Joe Johnes").one()
1199-
1200->>> print(person.id)
1201-1
1202->>> print(person.name)
1203-Joe Johnes
1204-
1205->>>
1206-}}}
1207+the person named "Joe Johnes"?
1208+
1209+.. doctest::
1210+
1211+ >>> person = store.find(Person, Person.name == u"Joe Johnes").one()
1212+
1213+ >>> print(person.id)
1214+ 1
1215+ >>> print(person.name)
1216+ Joe Johnes
1217
1218 The person is there! Yeah, ok, you were expecting it. :-)
1219
1220 We can also retrieve the object using its primary key.
1221
1222-{{{#!python
1223->>> print(store.get(Person, 1).name)
1224-Joe Johnes
1225-
1226-}}}
1227-
1228-==== Caching behavior ====
1229+.. doctest::
1230+
1231+ >>> print(store.get(Person, 1).name)
1232+ Joe Johnes
1233+
1234+Caching behavior
1235+----------------
1236
1237 One interesting thing is that this person is actually Joe, right? We've
1238 just added this object, so there's only one Joe, why would there be
1239 two different objects? There isn't.
1240
1241-{{{#!python
1242->>> person is joe
1243-True
1244+.. doctest::
1245
1246-}}}
1247+ >>> person is joe
1248+ True
1249
1250 What's going on behind the scenes is that each store has an object
1251 cache. When an object is linked to a store, it will be cached by
1252@@ -159,50 +150,52 @@
1253 will stay in memory inside the transaction, so that frequently used
1254 objects are not retrieved from the database too often.
1255
1256-==== Flushing ====
1257+Flushing
1258+--------
1259
1260 When we tried to find Joe in the database for the first time, we've
1261-noticed that the `id` property was magically assigned. This happened
1262+noticed that the ``id`` property was magically assigned. This happened
1263 because the object was flushed implicitly so that the operation would
1264 affect any pending changes as well.
1265
1266 Flushes may also happen explicitly.
1267
1268-{{{#!python
1269->>> mary = Person()
1270->>> mary.name = u"Mary Margaret"
1271->>> store.add(mary)
1272-<...Person object at 0x...>
1273-
1274->>> print(mary.id)
1275-None
1276->>> print(mary.name)
1277-Mary Margaret
1278-
1279->>> store.flush()
1280->>> print(mary.id)
1281-2
1282->>> print(mary.name)
1283-Mary Margaret
1284-
1285-}}}
1286-
1287-==== Changing objects with the Store ====
1288+.. doctest::
1289+
1290+ >>> mary = Person()
1291+ >>> mary.name = u"Mary Margaret"
1292+ >>> store.add(mary)
1293+ <...Person object at 0x...>
1294+
1295+ >>> print(mary.id)
1296+ None
1297+ >>> print(mary.name)
1298+ Mary Margaret
1299+
1300+ >>> store.flush()
1301+ >>> print(mary.id)
1302+ 2
1303+ >>> print(mary.name)
1304+ Mary Margaret
1305+
1306+Changing objects with the Store
1307+-------------------------------
1308
1309 Besides changing objects as usual, we can also benefit from the fact
1310 that objects are tied to a database to change them using expressions.
1311
1312-{{{#!python
1313->>> store.find(Person, Person.name == u"Mary Margaret").set(name=u"Mary Maggie")
1314->>> print(mary.name)
1315-Mary Maggie
1316+.. doctest::
1317
1318-}}}
1319+ >>> store.find(
1320+ ... Person, Person.name == u"Mary Margaret").set(name=u"Mary Maggie")
1321+ >>> print(mary.name)
1322+ Mary Maggie
1323
1324 This operation will touch every matching object in the database, and
1325 also objects that are alive in memory.
1326
1327-==== Committing ====
1328+Committing
1329+----------
1330
1331 Everything we've done so far is inside a transaction. At this point,
1332 we can either make these changes and any pending uncommitted changes
1333@@ -211,157 +204,150 @@
1334
1335 We'll commit them, with something as simple as
1336
1337-{{{#!python
1338->>> store.commit()
1339->>>
1340-}}}
1341+.. doctest::
1342+
1343+ >>> store.commit()
1344
1345 That was straightforward. Everything is still the way it was, but now
1346 changes are there "for real".
1347
1348-==== Rolling back ====
1349+Rolling back
1350+------------
1351
1352 Aborting changes is very straightforward as well.
1353
1354-{{{#!python
1355->>> joe.name = u"Tom Thomas"
1356->>>
1357-}}}
1358+.. doctest::
1359+
1360+ >>> joe.name = u"Tom Thomas"
1361
1362 Let's see if these changes are really being considered by Storm
1363 and by the database.
1364
1365-{{{#!python
1366->>> person = store.find(Person, Person.name == u"Tom Thomas").one()
1367->>> person is joe
1368-True
1369+.. doctest::
1370
1371-}}}
1372+ >>> person = store.find(Person, Person.name == u"Tom Thomas").one()
1373+ >>> person is joe
1374+ True
1375
1376 Yes, they are. Now, for the magic step (suspense music, please).
1377
1378-{{{#!python
1379->>> store.rollback()
1380->>>
1381-}}}
1382+.. doctest::
1383+
1384+ >>> store.rollback()
1385
1386 Erm.. nothing happened?
1387
1388 Actually, something happened.. with Joe. He's back!
1389
1390-{{{#!python
1391->>> print(joe.id)
1392-1
1393->>> print(joe.name)
1394-Joe Johnes
1395-
1396-}}}
1397-
1398-==== Constructors ====
1399+.. doctest::
1400+
1401+ >>> print(joe.id)
1402+ 1
1403+ >>> print(joe.name)
1404+ Joe Johnes
1405+
1406+Constructors
1407+------------
1408
1409 So, we've been working for too long with people only. Let's introduce
1410 a new kind of data in our model: companies. For the company, we'll
1411 use a constructor, just for the fun of it. It will be the simplest
1412 company class you've ever seen:
1413
1414-{{{#!python
1415->>> class Company(object):
1416-... __storm_table__ = "company"
1417-... id = Int(primary=True)
1418-... name = Unicode()
1419-...
1420-... def __init__(self, name):
1421-... self.name = name
1422+.. doctest:
1423
1424-}}}
1425+ >>> class Company(object):
1426+ ... __storm_table__ = "company"
1427+ ... id = Int(primary=True)
1428+ ... name = Unicode()
1429+ ...
1430+ ... def __init__(self, name):
1431+ ... self.name = name
1432
1433 Notice that the constructor parameter isn't optional. It could be
1434 optional, if we wanted, but our companies always have names.
1435
1436 Let's add the table for it.
1437
1438-{{{#!python
1439->>> store.execute("CREATE TABLE company "
1440-... "(id INTEGER PRIMARY KEY, name VARCHAR)", noresult=True)
1441+.. doctest::
1442
1443-}}}
1444+ >>> store.execute(
1445+ ... "CREATE TABLE company (id INTEGER PRIMARY KEY, name VARCHAR)",
1446+ ... noresult=True)
1447
1448 Then, create a new company.
1449
1450-{{{
1451->>> circus = Company(u"Circus Inc.")
1452-
1453->>> print(circus.id)
1454-None
1455->>> print(circus.name)
1456-Circus Inc.
1457-
1458-}}}
1459-
1460-The `id` is still undefined because we haven't flushed it. In fact,
1461-we haven't even '''added''' the company to the store. We'll do
1462+.. doctest::
1463+
1464+ >>> circus = Company(u"Circus Inc.")
1465+
1466+ >>> print(circus.id)
1467+ None
1468+ >>> print(circus.name)
1469+ Circus Inc.
1470+
1471+The ``id`` is still undefined because we haven't flushed it. In fact,
1472+we haven't even **added** the company to the store. We'll do
1473 that soon. Watch out.
1474
1475
1476-==== References and subclassing ====
1477+References and subclassing
1478+--------------------------
1479
1480 Now we want to assign some employees to our company. Rather than
1481 redoing the Person definition, we'll keep it as it is, since it's
1482 general, and will create a new subclass of it for employees, which
1483 include one extra field: the company id.
1484
1485-{{{#!python
1486->>> class Employee(Person):
1487-... __storm_table__ = "employee"
1488-... company_id = Int()
1489-... company = Reference(company_id, Company.id)
1490-...
1491-... def __init__(self, name):
1492-... self.name = name
1493-
1494-}}}
1495-
1496-Pay attention to that definiton for a moment. Notice that it doesn't
1497-define what's already in person, and introduces the `company_id`,
1498-and a `company` property, which is a reference to another class. It
1499+.. doctest::
1500+
1501+ >>> class Employee(Person):
1502+ ... __storm_table__ = "employee"
1503+ ... company_id = Int()
1504+ ... company = Reference(company_id, Company.id)
1505+ ...
1506+ ... def __init__(self, name):
1507+ ... self.name = name
1508+
1509+Pay attention to that definition for a moment. Notice that it doesn't
1510+define what's already in person, and introduces the ``company_id``,
1511+and a ``company`` property, which is a reference to another class. It
1512 also has a constructor, but which leaves the company alone.
1513
1514 As usual, we need a table. SQLite has no idea of what a foreign key is,
1515 so we'll not bother to define it.
1516
1517-{{{#!python
1518->>> store.execute("CREATE TABLE employee "
1519-... "(id INTEGER PRIMARY KEY, name VARCHAR, company_id INTEGER)",
1520-... noresult=True)
1521+.. doctest::
1522
1523-}}}
1524+ >>> store.execute(
1525+ ... "CREATE TABLE employee "
1526+ ... "(id INTEGER PRIMARY KEY, name VARCHAR, company_id INTEGER)",
1527+ ... noresult=True)
1528
1529 Let's give life to Ben now.
1530
1531-{{{#!python
1532->>> ben = store.add(Employee(u"Ben Bill"))
1533-
1534->>> print(ben.id)
1535-None
1536->>> print(ben.name)
1537-Ben Bill
1538->>> print(ben.company_id)
1539-None
1540-
1541-}}}
1542+.. doctest::
1543+
1544+ >>> ben = store.add(Employee(u"Ben Bill"))
1545+
1546+ >>> print(ben.id)
1547+ None
1548+ >>> print(ben.name)
1549+ Ben Bill
1550+ >>> print(ben.company_id)
1551+ None
1552
1553 We can see that they were not flushed yet. Even then, we can say
1554 that Bill works on Circus.
1555
1556-{{{#!python
1557->>> ben.company = circus
1558-
1559->>> print(ben.company_id)
1560-None
1561->>> print(ben.company.name)
1562-Circus Inc.
1563-
1564-}}}
1565+.. doctest::
1566+
1567+ >>> ben.company = circus
1568+
1569+ >>> print(ben.company_id)
1570+ None
1571+ >>> print(ben.company.name)
1572+ Circus Inc.
1573
1574 Of course, we still don't know the company id since it was not
1575 flushed to the database yet, and we didn't assign an id explicitly.
1576@@ -371,15 +357,14 @@
1577 explicitly), objects will get their ids, and any references are
1578 updated as well (before being flushed!).
1579
1580-{{{#!python
1581->>> store.flush()
1582-
1583->>> print(ben.company_id)
1584-1
1585->>> print(ben.company.name)
1586-Circus Inc.
1587-
1588-}}}
1589+.. doctest::
1590+
1591+ >>> store.flush()
1592+
1593+ >>> print(ben.company_id)
1594+ 1
1595+ >>> print(ben.company.name)
1596+ Circus Inc.
1597
1598 They're both flushed to the database. Now, notice that the Circus
1599 company wasn't added to the store explicitly in any moment. Storm
1600@@ -389,37 +374,34 @@
1601 Let's create another company to check something. This time we'll
1602 flush the store just after adding it.
1603
1604-{{{#!python
1605->>> sweets = store.add(Company(u"Sweets Inc."))
1606->>> store.flush()
1607->>> sweets.id
1608-2
1609-
1610-}}}
1611-
1612+.. doctest::
1613+
1614+ >>> sweets = store.add(Company(u"Sweets Inc."))
1615+ >>> store.flush()
1616+ >>> sweets.id
1617+ 2
1618
1619 Nice, we've already got the id of the new company. So, what would
1620-happen if we changed '''just the id''' for Ben's company?
1621-
1622-{{{#!python
1623->>> ben.company_id = 2
1624->>> print(ben.company.name)
1625-Sweets Inc.
1626->>> ben.company is sweets
1627-True
1628-
1629-}}}
1630-
1631-Hah! '''That''' wasn't expected, was it? ;-)
1632+happen if we changed **just the id** for Ben's company?
1633+
1634+.. doctest::
1635+
1636+ >>> ben.company_id = 2
1637+ >>> print(ben.company.name)
1638+ Sweets Inc.
1639+ >>> ben.company is sweets
1640+ True
1641+
1642+Hah! **That** wasn't expected, was it? ;-)
1643
1644 Let's commit everything.
1645
1646-{{{#!python
1647->>> store.commit()
1648->>>
1649-}}}
1650-
1651-==== Many-to-one reference sets ====
1652+.. doctest::
1653+
1654+ >>> store.commit()
1655+
1656+Many-to-one reference sets
1657+--------------------------
1658
1659 So, while our model says that employees work for a single company
1660 (we only design normal people here), companies may of course have
1661@@ -428,52 +410,48 @@
1662 We won't define the company again. Instead, we'll add a new attribute
1663 to the class.
1664
1665-{{{#!python
1666->>> Company.employees = ReferenceSet(Company.id, Employee.company_id)
1667->>>
1668-}}}
1669+.. doctest::
1670+
1671+ >>> Company.employees = ReferenceSet(Company.id, Employee.company_id)
1672
1673 Without any further work, we can already see which employees are
1674 working for a given company.
1675
1676-{{{#!python
1677->>> sweets.employees.count()
1678-1
1679-
1680->>> for employee in sweets.employees:
1681-... print(employee.id)
1682-... print(employee.name)
1683-... print(employee is ben)
1684-...
1685-1
1686-Ben Bill
1687-True
1688-
1689-}}}
1690+.. doctest::
1691+
1692+ >>> sweets.employees.count()
1693+ 1
1694+
1695+ >>> for employee in sweets.employees:
1696+ ... print(employee.id)
1697+ ... print(employee.name)
1698+ ... print(employee is ben)
1699+ ...
1700+ 1
1701+ Ben Bill
1702+ True
1703
1704 Let's create another employee, and add him to the company, rather
1705 than setting the company in the employee (it sounds better, at least).
1706
1707-{{{#!python
1708->>> mike = store.add(Employee(u"Mike Mayer"))
1709->>> sweets.employees.add(mike)
1710->>>
1711-}}}
1712+.. doctest::
1713+
1714+ >>> mike = store.add(Employee(u"Mike Mayer"))
1715+ >>> sweets.employees.add(mike)
1716
1717 That, of course, means that Mike's working for a company, and so it
1718 should be reflected elsewhere.
1719
1720-{{{#!python
1721->>> mike.company_id
1722-2
1723-
1724->>> mike.company is sweets
1725-True
1726-
1727-}}}
1728-
1729-
1730-==== Many-to-many reference sets and composed keys ====
1731+.. doctest::
1732+
1733+ >>> mike.company_id
1734+ 2
1735+
1736+ >>> mike.company is sweets
1737+ True
1738+
1739+Many-to-many reference sets and composed keys
1740+---------------------------------------------
1741
1742 We want to represent accountants in our model as well. Companies have
1743 accountants, but accountants may also attend several companies, so we'll
1744@@ -482,19 +460,18 @@
1745 Let's create a simple class to use with accountants, and the relationship
1746 class.
1747
1748-{{{#!python
1749->>> class Accountant(Person):
1750-... __storm_table__ = "accountant"
1751-... def __init__(self, name):
1752-... self.name = name
1753-
1754->>> class CompanyAccountant(object):
1755-... __storm_table__ = "company_accountant"
1756-... __storm_primary__ = "company_id", "accountant_id"
1757-... company_id = Int()
1758-... accountant_id = Int()
1759-
1760-}}}
1761+.. doctest::
1762+
1763+ >>> class Accountant(Person):
1764+ ... __storm_table__ = "accountant"
1765+ ... def __init__(self, name):
1766+ ... self.name = name
1767+
1768+ >>> class CompanyAccountant(object):
1769+ ... __storm_table__ = "company_accountant"
1770+ ... __storm_primary__ = "company_id", "accountant_id"
1771+ ... company_id = Int()
1772+ ... accountant_id = Int()
1773
1774 Hey, we've just declared a class with a composed key!
1775
1776@@ -503,44 +480,42 @@
1777 existent object. It may easily be defined at class definition
1778 time. Later we'll see another way to do that as well.
1779
1780-{{{#!python
1781->>> Company.accountants = ReferenceSet(Company.id,
1782-... CompanyAccountant.company_id,
1783-... CompanyAccountant.accountant_id,
1784-... Accountant.id)
1785+.. doctest::
1786
1787-}}}
1788+ >>> Company.accountants = ReferenceSet(Company.id,
1789+ ... CompanyAccountant.company_id,
1790+ ... CompanyAccountant.accountant_id,
1791+ ... Accountant.id)
1792
1793 Done! The order in which attributes were defined is important,
1794 but the logic should be pretty obvious.
1795
1796 We're missing some tables, at this point.
1797
1798-{{{#!python
1799->>> store.execute("CREATE TABLE accountant "
1800-... "(id INTEGER PRIMARY KEY, name VARCHAR)", noresult=True)
1801-...
1802-
1803->>> store.execute("CREATE TABLE company_accountant "
1804-... "(company_id INTEGER, accountant_id INTEGER,"
1805-... " PRIMARY KEY (company_id, accountant_id))", noresult=True)
1806-
1807-}}}
1808-
1809+.. doctest::
1810+
1811+ >>> store.execute(
1812+ ... "CREATE TABLE accountant (id INTEGER PRIMARY KEY, name VARCHAR)",
1813+ ... noresult=True)
1814+
1815+ >>> store.execute(
1816+ ... "CREATE TABLE company_accountant "
1817+ ... "(company_id INTEGER, accountant_id INTEGER,"
1818+ ... " PRIMARY KEY (company_id, accountant_id))",
1819+ ... noresult=True)
1820
1821 Let's give life to a couple of accountants, and register them
1822 in both companies.
1823
1824-{{{#!python
1825->>> karl = Accountant(u"Karl Kent")
1826->>> frank = Accountant(u"Frank Fourt")
1827-
1828->>> sweets.accountants.add(karl)
1829->>> sweets.accountants.add(frank)
1830-
1831->>> circus.accountants.add(frank)
1832->>>
1833-}}}
1834+.. doctest::
1835+
1836+ >>> karl = Accountant(u"Karl Kent")
1837+ >>> frank = Accountant(u"Frank Fourt")
1838+
1839+ >>> sweets.accountants.add(karl)
1840+ >>> sweets.accountants.add(frank)
1841+
1842+ >>> circus.accountants.add(frank)
1843
1844 That's it! Really! Notice that we didn't even have to add them to
1845 the store, since it happens implicitly by linking to the other object
1846@@ -549,49 +524,47 @@
1847
1848 We can now check them.
1849
1850-{{{
1851->>> sweets.accountants.count()
1852-2
1853-
1854->>> circus.accountants.count()
1855-1
1856-
1857-}}}
1858-
1859-Even though we didn't use the Company``Accountant object explicitly,
1860+.. doctest::
1861+
1862+ >>> sweets.accountants.count()
1863+ 2
1864+
1865+ >>> circus.accountants.count()
1866+ 1
1867+
1868+Even though we didn't use the ``CompanyAccountant`` object explicitly,
1869 we can check it if we're really curious.
1870
1871-{{{#!python
1872->>> store.get(CompanyAccountant, (sweets.id, frank.id))
1873-<...CompanyAccountant object at 0x...>
1874-
1875-}}}
1876-
1877-Notice that we pass a tuple for the `get()` method, due to the
1878-composed key.
1879+.. doctest::
1880+
1881+ >>> store.get(CompanyAccountant, (sweets.id, frank.id))
1882+ <...CompanyAccountant object at 0x...>
1883+
1884+Notice that we pass a tuple for the :py:meth:`~storm.store.Store.get`
1885+method, due to the composed key.
1886
1887 If we wanted to know for which companies accountants are working,
1888 we could easily define a reversed relationship:
1889
1890-{{{#!python
1891->>> Accountant.companies = ReferenceSet(Accountant.id,
1892-... CompanyAccountant.accountant_id,
1893-... CompanyAccountant.company_id,
1894-... Company.id)
1895-
1896->>> for name in sorted(company.name for company in frank.companies):
1897-... print(name)
1898-Circus Inc.
1899-Sweets Inc.
1900-
1901->>> for company in karl.companies:
1902-... print(company.name)
1903-Sweets Inc.
1904-
1905-}}}
1906-
1907-
1908-==== Joins ====
1909+.. doctest::
1910+
1911+ >>> Accountant.companies = ReferenceSet(Accountant.id,
1912+ ... CompanyAccountant.accountant_id,
1913+ ... CompanyAccountant.company_id,
1914+ ... Company.id)
1915+
1916+ >>> for name in sorted(company.name for company in frank.companies):
1917+ ... print(name)
1918+ Circus Inc.
1919+ Sweets Inc.
1920+
1921+ >>> for company in karl.companies:
1922+ ... print(company.name)
1923+ Sweets Inc.
1924+
1925+
1926+Joins
1927+-----
1928
1929 Since we've got some nice data to play with, let's try to make a
1930 few interesting queries.
1931@@ -601,209 +574,204 @@
1932
1933 First, with an implicit join.
1934
1935-{{{#!python
1936->>> result = store.find(Company,
1937-... Employee.company_id == Company.id,
1938-... Employee.name.like(u"Ben %"))
1939-...
1940-
1941->>> for company in result:
1942-... print(company.name)
1943-Sweets Inc.
1944-
1945-}}}
1946+.. doctest::
1947+
1948+ >>> result = store.find(Company,
1949+ ... Employee.company_id == Company.id,
1950+ ... Employee.name.like(u"Ben %"))
1951+
1952+ >>> for company in result:
1953+ ... print(company.name)
1954+ Sweets Inc.
1955
1956 Then, we can also do an explicit join. This is interesting for mapping
1957 complex SQL joins to Storm queries.
1958
1959-{{{#!python
1960->>> origin = [Company, Join(Employee, Employee.company_id == Company.id)]
1961->>> result = store.using(*origin).find(Company, Employee.name.like(u"Ben %"))
1962-
1963->>> for company in result:
1964-... print(company.name)
1965-Sweets Inc.
1966-
1967-}}}
1968-
1969+.. doctest::
1970+
1971+ >>> origin = [Company, Join(Employee, Employee.company_id == Company.id)]
1972+ >>> result = store.using(*origin).find(
1973+ ... Company, Employee.name.like(u"Ben %"))
1974+
1975+ >>> for company in result:
1976+ ... print(company.name)
1977+ Sweets Inc.
1978
1979 If we already had the company, and wanted to know which of his employees
1980 were named Ben, that'd have been easier.
1981
1982-{{{#!python
1983->>> result = sweets.employees.find(Employee.name.like(u"Ben %"))
1984-
1985->>> for employee in result:
1986-... print(employee.name)
1987-Ben Bill
1988-
1989-}}}
1990-
1991-
1992-==== Sub-selects ====
1993+.. doctest::
1994+
1995+ >>> result = sweets.employees.find(Employee.name.like(u"Ben %"))
1996+
1997+ >>> for employee in result:
1998+ ... print(employee.name)
1999+ Ben Bill
2000+
2001+
2002+Sub-selects
2003+-----------
2004
2005 Suppose we want to find all accountants that aren't associated with a
2006 company. We can use a sub-select to get the data we want.
2007
2008-{{{#!python
2009->>> laura = Accountant(u"Laura Montgomery")
2010->>> store.add(laura)
2011-<...Accountant ...>
2012-
2013->>> subselect = Select(CompanyAccountant.accountant_id, distinct=True)
2014->>> result = store.find(Accountant, Not(Accountant.id.is_in(subselect)))
2015->>> result.one() is laura
2016-True
2017->>>
2018-}}}
2019-
2020-
2021-==== Ordering and limiting results ====
2022+.. doctest::
2023+
2024+ >>> laura = Accountant(u"Laura Montgomery")
2025+ >>> store.add(laura)
2026+ <...Accountant ...>
2027+
2028+ >>> subselect = Select(CompanyAccountant.accountant_id, distinct=True)
2029+ >>> result = store.find(Accountant, Not(Accountant.id.is_in(subselect)))
2030+ >>> result.one() is laura
2031+ True
2032+
2033+
2034+Ordering and limiting results
2035+-----------------------------
2036
2037 Ordering and limiting results obtained are certainly among the
2038 simplest and yet most wanted features for such tools, so we want
2039 to make them very easy to understand and use, of course.
2040
2041-A code of line is worth a thousand words, so here are a few examples
2042+A line of code is worth a thousand words, so here are a few examples
2043 that demonstrate how it works:
2044
2045-{{{
2046->>> garry = store.add(Employee(u"Garry Glare"))
2047-
2048->>> result = store.find(Employee)
2049-
2050->>> for employee in result.order_by(Employee.name):
2051-... print(employee.name)
2052-Ben Bill
2053-Garry Glare
2054-Mike Mayer
2055-
2056->>> for employee in result.order_by(Desc(Employee.name)):
2057-... print(employee.name)
2058-Mike Mayer
2059-Garry Glare
2060-Ben Bill
2061-
2062->>> for employee in result.order_by(Employee.name)[:2]:
2063-... print(employee.name)
2064-Ben Bill
2065-Garry Glare
2066-
2067-}}}
2068-
2069-
2070-==== Multiple types with one query ====
2071+.. doctest::
2072+
2073+ >>> garry = store.add(Employee(u"Garry Glare"))
2074+
2075+ >>> result = store.find(Employee)
2076+
2077+ >>> for employee in result.order_by(Employee.name):
2078+ ... print(employee.name)
2079+ Ben Bill
2080+ Garry Glare
2081+ Mike Mayer
2082+
2083+ >>> for employee in result.order_by(Desc(Employee.name)):
2084+ ... print(employee.name)
2085+ Mike Mayer
2086+ Garry Glare
2087+ Ben Bill
2088+
2089+ >>> for employee in result.order_by(Employee.name)[:2]:
2090+ ... print(employee.name)
2091+ Ben Bill
2092+ Garry Glare
2093+
2094+
2095+Multiple types with one query
2096+-----------------------------
2097
2098 Sometimes, it may be interesting to retrieve more than one object involved
2099 in a given query. Imagine, for instance, that besides knowing which
2100 companies have an employee named Ben, we also want to know who is the
2101 employee. This may be achieved with a query like follows:
2102
2103-{{{#!python
2104->>> result = store.find((Company, Employee),
2105-... Employee.company_id == Company.id,
2106-... Employee.name.like(u"Ben %"))
2107-
2108->>> for company, employee in result:
2109-... print(company.name)
2110-... print(employee.name)
2111-Sweets Inc.
2112-Ben Bill
2113-
2114-}}}
2115-
2116-
2117-==== The Storm base class ====
2118+.. doctest::
2119+
2120+ >>> result = store.find((Company, Employee),
2121+ ... Employee.company_id == Company.id,
2122+ ... Employee.name.like(u"Ben %"))
2123+
2124+ >>> for company, employee in result:
2125+ ... print(company.name)
2126+ ... print(employee.name)
2127+ Sweets Inc.
2128+ Ben Bill
2129+
2130+
2131+The Storm base class
2132+--------------------
2133
2134 So far we've been defining our references and reference sets using
2135 classes and their properties. This has some advantages, like being
2136 easier to debug, but also has some disadvantages, such as requiring
2137-classes to be present in the local scope, what potentially leads to
2138+classes to be present in the local scope, which potentially leads to
2139 circular import issues.
2140
2141 To prevent that kind of situation, Storm supports defining these
2142 references using the stringified version of the class and property
2143 names. The only inconvenience of doing so is that all involved
2144-classes must inherit from the `Storm` base class.
2145+classes must inherit from the :py:class:`~storm.base.Storm` base class.
2146
2147 Let's define some new classes to show that. To expose the point,
2148 we'll refer to a class before it's actually defined.
2149
2150-{{{#!python
2151->>> class Country(Storm):
2152-... __storm_table__ = "country"
2153-... id = Int(primary=True)
2154-... name = Unicode()
2155-... currency_id = Int()
2156-... currency = Reference(currency_id, "Currency.id")
2157-
2158->>> class Currency(Storm):
2159-... __storm_table__ = "currency"
2160-... id = Int(primary=True)
2161-... symbol = Unicode()
2162-
2163->>> store.execute("CREATE TABLE country "
2164-... "(id INTEGER PRIMARY KEY, name VARCHAR, currency_id INTEGER)",
2165-... noresult=True)
2166-
2167->>> store.execute("CREATE TABLE currency "
2168-... "(id INTEGER PRIMARY KEY, symbol VARCHAR)", noresult=True)
2169-
2170-}}}
2171-
2172+.. doctest::
2173+
2174+ >>> class Country(Storm):
2175+ ... __storm_table__ = "country"
2176+ ... id = Int(primary=True)
2177+ ... name = Unicode()
2178+ ... currency_id = Int()
2179+ ... currency = Reference(currency_id, "Currency.id")
2180+
2181+ >>> class Currency(Storm):
2182+ ... __storm_table__ = "currency"
2183+ ... id = Int(primary=True)
2184+ ... symbol = Unicode()
2185+
2186+ >>> store.execute(
2187+ ... "CREATE TABLE country "
2188+ ... "(id INTEGER PRIMARY KEY, name VARCHAR, currency_id INTEGER)",
2189+ ... noresult=True)
2190+
2191+ >>> store.execute(
2192+ ... "CREATE TABLE currency (id INTEGER PRIMARY KEY, symbol VARCHAR)",
2193+ ... noresult=True)
2194
2195 Now, let's see if it works.
2196
2197-{{{#!python
2198->>> real = store.add(Currency())
2199->>> real.id = 1
2200->>> real.symbol = u"BRL"
2201-
2202->>> brazil = store.add(Country())
2203->>> brazil.name = u"Brazil"
2204->>> brazil.currency_id = 1
2205-
2206->>> print(brazil.currency.symbol)
2207-BRL
2208-
2209-}}}
2210+.. doctest::
2211+
2212+ >>> real = store.add(Currency())
2213+ >>> real.id = 1
2214+ >>> real.symbol = u"BRL"
2215+
2216+ >>> brazil = store.add(Country())
2217+ >>> brazil.name = u"Brazil"
2218+ >>> brazil.currency_id = 1
2219+
2220+ >>> print(brazil.currency.symbol)
2221+ BRL
2222
2223 Questions!? ;-)
2224
2225
2226-==== Loading hook ====
2227+Loading hook
2228+------------
2229
2230 Storm allows classes to define a few different hooks are called
2231 to act when certain things happen. One of the interesting hooks
2232-available is the `__storm_loaded__` one.
2233+available is the ``__storm_loaded__`` one.
2234
2235 Let's play with it. We'll define a temporary subclass of Person
2236 for that.
2237
2238-{{{#!python
2239->>> class PersonWithHook(Person):
2240-... def __init__(self, name):
2241-... print("Creating %s" % name)
2242-... self.name = name
2243-...
2244-... def __storm_loaded__(self):
2245-... print("Loaded %s" % self.name)
2246-
2247-
2248->>> earl = store.add(PersonWithHook(u"Earl Easton"))
2249-Creating Earl Easton
2250-
2251->>> earl = store.find(PersonWithHook, name=u"Earl Easton").one()
2252-
2253->>> store.invalidate(earl)
2254->>> del earl
2255->>> import gc
2256->>> collected = gc.collect()
2257-
2258->>> earl = store.find(PersonWithHook, name=u"Earl Easton").one()
2259-Loaded Earl Easton
2260-
2261-}}}
2262+.. doctest::
2263+
2264+ >>> class PersonWithHook(Person):
2265+ ... def __init__(self, name):
2266+ ... print("Creating %s" % name)
2267+ ... self.name = name
2268+ ...
2269+ ... def __storm_loaded__(self):
2270+ ... print("Loaded %s" % self.name)
2271+
2272+ >>> earl = store.add(PersonWithHook(u"Earl Easton"))
2273+ Creating Earl Easton
2274+
2275+ >>> earl = store.find(PersonWithHook, name=u"Earl Easton").one()
2276+
2277+ >>> store.invalidate(earl)
2278+ >>> del earl
2279+ >>> import gc
2280+ >>> collected = gc.collect()
2281+
2282+ >>> earl = store.find(PersonWithHook, name=u"Earl Easton").one()
2283+ Loaded Earl Easton
2284
2285 Note that in the first find, nothing was called, since the object
2286 was still in memory and cached. Then, we invalidated the object
2287@@ -813,52 +781,53 @@
2288 called (and not the constructor!).
2289
2290
2291-==== Executing expressions ====
2292+Executing expressions
2293+---------------------
2294
2295 Storm also offers a way to execute expressions in a
2296 database-agnostic way, when that's necessary.
2297
2298 For instance:
2299
2300-{{{#!python
2301->>> result = store.execute(Select(Person.name, Person.id == 1))
2302->>> (name,) = result.get_one()
2303->>> print(name)
2304-Joe Johnes
2305+.. doctest::
2306
2307-}}}
2308+ >>> result = store.execute(Select(Person.name, Person.id == 1))
2309+ >>> (name,) = result.get_one()
2310+ >>> print(name)
2311+ Joe Johnes
2312
2313 This mechanism is used internally by Storm itself to implement
2314 the higher level features.
2315
2316
2317-==== Auto-reloading values ====
2318+Auto-reloading values
2319+---------------------
2320
2321 Storm offers some special values that may be assigned to attributes
2322-under its control. One of these values is `AutoReload`. When used,
2323-it will make the object automatically reload the value from the
2324-database when touched. Even primary keys may benefit from its use,
2325-as shown below.
2326-
2327-{{{
2328->>> from storm.locals import AutoReload
2329-
2330->>> ruy = store.add(Person())
2331->>> ruy.name = u"Ruy"
2332->>> print(ruy.id)
2333-None
2334-
2335->>> ruy.id = AutoReload
2336->>> print(ruy.id)
2337-4
2338-
2339-}}}
2340+under its control. One of these values is
2341+:py:data:`~storm.store.AutoReload`. When used, it will make the
2342+object automatically reload the value from the database when touched.
2343+Even primary keys may benefit from its use, as shown below.
2344+
2345+.. doctest::
2346+
2347+ >>> from storm.locals import AutoReload
2348+
2349+ >>> ruy = store.add(Person())
2350+ >>> ruy.name = u"Ruy"
2351+ >>> print(ruy.id)
2352+ None
2353+
2354+ >>> ruy.id = AutoReload
2355+ >>> print(ruy.id)
2356+ 4
2357
2358 This may be set as the default value for any attribute, making the
2359 object be automatically flushed if necessary.
2360
2361
2362-==== Expression values ====
2363+Expression values
2364+-----------------
2365
2366 Besides auto-reloading, it's also possible to assign what we call
2367 a "lazy expression" to an attribute. Such expressions are flushed
2368@@ -867,60 +836,57 @@
2369
2370 For instance:
2371
2372-{{{#!python
2373-from storm.locals import SQL
2374-
2375->>> ruy.name = SQL("(SELECT name || ? FROM person WHERE id=4)", (" Ritcher",))
2376->>> print(ruy.name)
2377-Ruy Ritcher
2378-
2379-}}}
2380-
2381-Notice that this is just an example of what '''may''' be done. There's
2382+.. doctest::
2383+
2384+ >>> ruy.name = SQL(
2385+ ... "(SELECT name || ? FROM person WHERE id=4)", (" Ritcher",))
2386+ >>> print(ruy.name)
2387+ Ruy Ritcher
2388+
2389+Notice that this is just an example of what **may** be done. There's
2390 no need to write SQL statements this way, if you don't want to. You may
2391 also use class-based SQL expressions provided in Storm, or even
2392 not use lazy expressions at all.
2393
2394
2395-==== Aliases ====
2396+Aliases
2397+-------
2398
2399 So now let's say that we want to find every pair of people that work
2400-for the same company. I have no idea about why one would ''want'' to
2401+for the same company. I have no idea about why one would *want* to
2402 do that, but that's a good case for us to exercise aliases.
2403
2404-First, we import `ClassAlias` into the local namespace (''mental note:
2405-this should be in storm.locals as well''), and create a reference to it.
2406-
2407-{{{#!python
2408->>> from storm.info import ClassAlias
2409->>> AnotherEmployee = ClassAlias(Employee)
2410-
2411-}}}
2412+First, we create an alias for the `Employee` class.
2413+
2414+.. doctest::
2415+
2416+ >>> from storm.info import ClassAlias
2417+ >>> AnotherEmployee = ClassAlias(Employee)
2418
2419 Nice, isn't it?
2420
2421 Now we can easily make the query we want, in a straightforward way:
2422
2423-{{{#!python
2424->>> result = store.find((Employee, AnotherEmployee),
2425-... Employee.company_id == AnotherEmployee.company_id,
2426-... Employee.id > AnotherEmployee.id)
2427-
2428->>> for employee1, employee2 in result:
2429-... print(employee1.name)
2430-... print(employee2.name)
2431-Mike Mayer
2432-Ben Bill
2433-
2434-}}}
2435+.. doctest::
2436+
2437+ >>> result = store.find((Employee, AnotherEmployee),
2438+ ... Employee.company_id == AnotherEmployee.company_id,
2439+ ... Employee.id > AnotherEmployee.id)
2440+
2441+ >>> for employee1, employee2 in result:
2442+ ... print(employee1.name)
2443+ ... print(employee2.name)
2444+ Mike Mayer
2445+ Ben Bill
2446
2447 Woah! Mike and Ben work for the same company!
2448
2449-(Quiz for the attent reader: why is ''greater than'' being used in
2450+(Quiz for the attentive reader: why is *greater than* being used in
2451 the query above?)
2452
2453
2454-==== Debugging ====
2455+Debugging
2456+---------
2457
2458 Sometimes you just need to see which statements Storm is executing. A
2459 debug tracer built on top of Storm's tracing system can be used to see
2460@@ -930,37 +896,33 @@
2461 provided. Statements are logged to sys.stderr by default, but a
2462 custom stream may also be used.
2463
2464-{{{#!python
2465->>> import sys
2466->>> from storm.tracer import debug
2467-
2468->>> debug(True, stream=sys.stdout)
2469->>> result = store.find((Employee, AnotherEmployee),
2470-... Employee.company_id == AnotherEmployee.company_id,
2471-... Employee.id > AnotherEmployee.id)
2472->>> list(result)
2473-[...] EXECUTE: ...'SELECT employee.company_id, employee.id, employee.name, "...".company_id, "...".id, "...".name FROM employee, employee AS "..." WHERE employee.company_id = "...".company_id AND employee.id > "...".id', ()
2474-[...] DONE
2475-[(<...Employee object at ...>, <...Employee object at ...>)]
2476-
2477->>> debug(False)
2478->>> list(result)
2479-[(<...Employee object at ...>, <...Employee object at ...>)]
2480-
2481-}}}
2482-
2483-
2484-==== Much more! ====
2485+.. doctest::
2486+
2487+ >>> import sys
2488+ >>> from storm.tracer import debug
2489+
2490+ >>> debug(True, stream=sys.stdout)
2491+ >>> result = store.find((Employee, AnotherEmployee),
2492+ ... Employee.company_id == AnotherEmployee.company_id,
2493+ ... Employee.id > AnotherEmployee.id)
2494+ >>> list(result)
2495+ [...] EXECUTE: ...'SELECT employee.company_id, employee.id, employee.name, "...".company_id, "...".id, "...".name FROM employee, employee AS "..." WHERE employee.company_id = "...".company_id AND employee.id > "...".id', ()
2496+ [...] DONE
2497+ [(<...Employee object at ...>, <...Employee object at ...>)]
2498+
2499+ >>> debug(False)
2500+ >>> list(result)
2501+ [(<...Employee object at ...>, <...Employee object at ...>)]
2502+
2503+
2504+Much more!
2505+----------
2506
2507 There's a lot more about Storm to be shown. This tutorial is just a
2508-way to get initiated on some of the concepts. While your questions
2509-are not answered somewhere else, feel free to ask them in the mailing
2510+way to get initiated on some of the concepts. If your questions are
2511+not answered somewhere else, feel free to ask them in the mailing
2512 list.
2513
2514-
2515-==== Clean up ====
2516-
2517+..
2518 >>> Currency._storm_property_registry.clear()
2519 >>> Country._storm_property_registry.clear()
2520-
2521-## vim:ts=4:sw=4:et:ft=moin1_5
2522
2523=== renamed file 'storm/tests/zope/README.txt' => 'storm/docs/zope.rst'
2524--- storm/tests/zope/README.txt 2020-05-26 10:31:46 +0000
2525+++ storm/docs/zope.rst 2020-05-26 10:31:46 +0000
2526@@ -1,31 +1,38 @@
2527-Copyright (c) 2006, 2007 Canonical
2528-
2529-Written by Jamshed Kakar <jkakar@kakar.ca>
2530-
2531-This file is part of Storm Object Relational Mapper.
2532-
2533-Storm is free software; you can redistribute it and/or modify
2534-it under the terms of the GNU Lesser General Public License as
2535-published by the Free Software Foundation; either version 2.1 of
2536-the License, or (at your option) any later version.
2537-
2538-Storm is distributed in the hope that it will be useful,
2539-but WITHOUT ANY WARRANTY; without even the implied warranty of
2540-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2541-GNU Lesser General Public License for more details.
2542-
2543-You should have received a copy of the GNU Lesser General Public License
2544-along with this program. If not, see <http://www.gnu.org/licenses/>.
2545-
2546-
2547-Introduction
2548-------------
2549-
2550-The storm.zope package contains the ZStorm utility which provides
2551+..
2552+ Copyright (c) 2006, 2007 Canonical
2553+
2554+ Written by Jamshed Kakar <jkakar@kakar.ca>
2555+
2556+ This file is part of Storm Object Relational Mapper.
2557+
2558+ Storm is free software; you can redistribute it and/or modify
2559+ it under the terms of the GNU Lesser General Public License as
2560+ published by the Free Software Foundation; either version 2.1 of
2561+ the License, or (at your option) any later version.
2562+
2563+ Storm is distributed in the hope that it will be useful,
2564+ but WITHOUT ANY WARRANTY; without even the implied warranty of
2565+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2566+ GNU Lesser General Public License for more details.
2567+
2568+ You should have received a copy of the GNU Lesser General Public License
2569+ along with this program. If not, see <http://www.gnu.org/licenses/>.
2570+
2571+
2572+Zope integration
2573+================
2574+
2575+The ``storm.zope`` package contains the ZStorm utility which provides
2576 seamless integration between Storm and Zope 3's transaction system.
2577 Setting up ZStorm is quite easy. In most cases, you want to include
2578-storm/zope/configure.zcml in your application. For the purposes of
2579-this doctest we'll register ZStorm manually.
2580+``storm/zope/configure.zcml`` in your application, which you would normally
2581+do in ZCML as follows:
2582+
2583+.. code-block:: xml
2584+
2585+ <include package="storm.zope" />
2586+
2587+For the purposes of this doctest we'll register ZStorm manually.
2588
2589 >>> from zope.component import provideUtility, getUtility
2590 >>> import transaction
2591@@ -48,8 +55,8 @@
2592 >>> zstorm.set_default_uri("test", "sqlite:")
2593
2594 Setting a default URI for stores isn't strictly required. We could
2595-pass it as the second argument to zstorm.get. Providing a default URI
2596-makes it possible to use zstorm.get more easily; this is especially
2597+pass it as the second argument to ``zstorm.get``. Providing a default URI
2598+makes it possible to use ``zstorm.get`` more easily; this is especially
2599 handy when multiple threads are used as we'll see further on.
2600
2601 >>> store = zstorm.get("test")
2602@@ -97,7 +104,7 @@
2603 ... """)
2604 >>> store.commit()
2605
2606-We'll need a Person class to use with this database.
2607+We'll need a ``Person`` class to use with this database.
2608
2609 >>> from storm.locals import Storm, Int, Unicode
2610
2611@@ -118,7 +125,7 @@
2612 <...Person object at ...>
2613 >>> transaction.commit()
2614
2615-Notice that we're not using store.commit directly; we're using Zope's
2616+Notice that we're not using ``store.commit`` directly; we're using Zope's
2617 transaction system. Let's make sure it worked.
2618
2619 >>> store.rollback()
2620@@ -137,7 +144,7 @@
2621 >>> store.add(Person(u"Imposter!"))
2622 <...Person object at ...>
2623
2624-At this point a store.find should return the new object.
2625+At this point a ``store.find`` should return the new object.
2626
2627 >>> for name in sorted(person.name for person in store.find(Person)):
2628 ... print(name)
2629@@ -145,7 +152,7 @@
2630 John Doe
2631
2632 All this means is that the data has been flushed to the database; it's
2633-still not committed. If we abort the transaction the new Person
2634+still not committed. If we abort the transaction the new ``Person``
2635 object should disappear.
2636
2637 >>> transaction.abort()
2638@@ -165,9 +172,11 @@
2639 stanza similar to the following to your ZCML configuration to setup a
2640 named store.
2641
2642+.. code-block:: xml
2643+
2644 <store name="test" uri="sqlite:" />
2645
2646-With that in place getUtility(IZStorm).get("test") will return the
2647+With that in place ``getUtility(IZStorm).get("test")`` will return the
2648 store named "test".
2649
2650
2651@@ -280,7 +289,7 @@
2652 ... self.person = person
2653 ... self.team = team
2654
2655- >>> class Team(Person):
2656+ >>> class Team(Storm):
2657 ...
2658 ... __storm_table__ = "team"
2659 ...
2660@@ -324,8 +333,8 @@
2661 ResultSet interfaces
2662 --------------------
2663
2664-Query results provide IResultSet (or ISQLObjectResultSet if SQLObject's
2665-compatibility layer is used).
2666+Query results provide ``IResultSet`` (or ``ISQLObjectResultSet`` if
2667+SQLObject's compatibility layer is used).
2668
2669 >>> from storm.zope.interfaces import IResultSet, ISQLObjectResultSet
2670 >>> from storm.store import EmptyResultSet, ResultSet
2671@@ -339,13 +348,9 @@
2672 True
2673
2674
2675-The End
2676--------
2677-
2678+..
2679 >>> Team._storm_property_registry.clear()
2680 >>> TeamMembership._storm_property_registry.clear()
2681 >>> Person._storm_property_registry.clear()
2682 >>> transaction.abort()
2683 >>> zstorm._reset()
2684-
2685-# vim:ts=4:sw=4:et
2686
2687=== modified file 'storm/tests/__init__.py'
2688--- storm/tests/__init__.py 2019-08-11 17:51:37 +0000
2689+++ storm/tests/__init__.py 2020-05-26 10:31:46 +0000
2690@@ -29,6 +29,7 @@
2691 ]
2692
2693 import doctest
2694+from itertools import chain
2695 import os
2696 import unittest
2697
2698@@ -71,14 +72,16 @@
2699 topdir = os.path.abspath(
2700 os.path.join(os.path.dirname(__file__), os.pardir, os.pardir))
2701 testdir = os.path.dirname(__file__)
2702+ docdir = os.path.join(os.path.dirname(testdir), "docs")
2703 testpaths = set(testpaths)
2704- for root, dirnames, filenames in os.walk(testdir):
2705+ for root, dirnames, filenames in chain(os.walk(testdir), os.walk(docdir)):
2706 for filename in filenames:
2707 filepath = os.path.join(root, filename)
2708 relpath = filepath[len(topdir)+1:]
2709
2710 if (filename == "__init__.py" or filename.endswith(".pyc") or
2711- relpath == os.path.join("storm", "tests", "conftest.py")):
2712+ relpath == os.path.join("storm", "docs", "conf.py") or
2713+ relpath == os.path.join("storm", "tests", "conftest.py")):
2714 # Skip non-tests.
2715 continue
2716
2717@@ -95,10 +98,9 @@
2718 module = __import__(modpath, None, None, [""])
2719 suite.addTest(
2720 unittest.defaultTestLoader.loadTestsFromModule(module))
2721- elif filename.endswith(".txt"):
2722+ elif filename.endswith(".rst"):
2723 load_test = True
2724- if relpath == os.path.join(
2725- "storm", "tests", "zope", "README.txt"):
2726+ if relpath == os.path.join("storm", "docs", "zope.rst"):
2727 # Special case the inclusion of the Zope-dependent
2728 # ZStorm doctest.
2729 import storm.tests.zope as ztest
2730
2731=== modified file 'tox.ini'
2732--- tox.ini 2019-11-21 01:57:01 +0000
2733+++ tox.ini 2020-05-26 10:31:46 +0000
2734@@ -5,6 +5,7 @@
2735 py36-{cextensions,nocextensions}
2736 py37-{cextensions,nocextensions}
2737 py38-{cextensions,nocextensions}
2738+ docs
2739
2740 [testenv]
2741 deps =
2742@@ -15,3 +16,11 @@
2743 nocextensions: STORM_CEXTENSIONS = 0
2744 commands =
2745 python dev/test {posargs}
2746+
2747+[testenv:docs]
2748+basepython =
2749+ python3
2750+commands =
2751+ sphinx-build -b html -d storm/docs/_build/doctrees storm/docs storm/docs/_build/html
2752+deps =
2753+ .[doc]

Subscribers

People subscribed via source and target branches

to status/vote changes: