if it’s giving you problems, it may be because the __module__ attribute is not 
set in 0.9, that is set in 1.0.

SQLAlchemy since the beginning has had this kind of pattern:

def a_thing(*args):
    “””Produce a Thing”””

    if (condition):
       return Thing(something)
    else:
       return Thing(something else)

that is, lots of factory functions separate from the classes.    One reason is 
that this allows the functions to sometimes return composite objects at once, 
or objects with various settings; another is that we can change what the 
functions return without impacting compatibility.   Being able to switch around 
like this is key, so for SQLAlchemy’s “DSL”-ness, the majority of constructs we 
deal with are invoked from lower case names.

But in practice, the majority of these function / class combos turned out to be 
a lot of extra verbiage; in most cases, it was just a function that called into 
the class constructor. 

So what do we do with that.   We start naming classes with the all_lowercase() 
names is one, I see this in the Python stdlib a lot.  But I *hate* when actual 
Python classes are named with all_lowercase().     Additionally, this seriously 
screws up documentation; it means the file location and/or __module__ of all 
these all-in-one pointers, which is what Sphinx autodoc uses for documentation, 
are now deep within sqlalchemy.foo.bar.bat, and I don’t want the documentation 
of the public API to be exposed to all that depth, or to changes in location; 
all major functions are reported in coarse-grained buckets, this is more on the 
Core side, like “sqlalchemy.sql.expression.<name>”.

Additionally it also breaks code that does any kind of introspection, since 
functions and classes via their constructor are inspected entirely differently. 
 If in Sphinx I have a reference like :class:`.mapper`, and then later mapper() 
becomes a function that calls to Mapper, now all the :class:`.mapper` links are 
broken.    We had this problem big time when i changed sessionmaker() to be a 
class.  Establishing a rule that any direct-to-class function needs to just be 
the class constructor would be unmanageable.

So that’s the end of part one - for the Core API, and a large chunk of ORM, we 
stick with all functions in the API, which call upon classes.

Now is part two.

So then we have this:

def text(…):
    “””Produce a TextClause construct

    docs docs docs

    params

    “””
   return TextClause()


text() is the front-facing API, it’s essential that it has those docs.  But 
then we have this:

class TextClause(ClauseElement):
    “””A TextClause construct”””

    def __init__(…):
       “””Produce a TextClause construct.

       docs docs docs (or no docs)

       params (or no params)
       “””

Problem 1 - In order for me to document TextClause.__init__(), I have to *copy* 
all the documentation almost word for word from that of text().   Or leave 
__init__ totally without documentation.  Yuk.

Problem 2 - As far as Sphinx API documentation, I’m looking to keep these docs 
as generated as possible, that is, I want to use straight “..autofunction::” 
and that’s it - I don’t want to type the documentation out a *third* time in an 
.rst file! (which is what python.org does, see 
https://docs.python.org/2/_sources/library/collections.txt, triple yuk!!)   So 
the file location and/or __module__ of these objects is what Sphinx uses to 
report where you’d be importing this function from.  So in order for me to have 
lots and lots of functions in sqlalchemy.expression that are documented, 
sqlalchemy.expression needs to be *huge*, because everything has to be there, 
and we have all those long docstrings, that were only getting longer.    

Problem 3 - I have to maintain the *args and **kwargs of all the public API 
functions distinctly in two different places, the function and the constructor, 
as well as the list of :paramref:.  These can get out of sync.

Problem 4 - Now that our API documentation has become very rich, we have all of 
these classes in the documentation as well.     So users can see these things.  
 With the above, they will see documentation for “text()”, “TextClause”, and 
“TextClause.__init__() -> Produce a new TextClause() construct”””.     So then, 
wait which API do I call, do I call text(), or TextClause()?  They are both 
documented?  What’s the one obvious way to do it ?


public_factory() solves all of these problems amazingly in one fell swoop, and 
literally nobody has noticed (except for people dealing with the code or with 
the bug regarding __module__).

With public_factory(), I can:

1. write the docs for the function / class in just one place, in the __init__ 
method, and they are available immediately in the SQLAlchemy function 
interface.    

2. There’s no need to have huge docstrings filling up the one giant namespace 
where you want the actual public API functions to be imported from / documented 
as part of.

3. functions stay as functions, classes as classes, with no need to build out a 
separate “def foo():  “””docs docs docs”””” elsewhere, in the case that the 
function just calls on the class, this is just the one liners you see in 
sqlalchemy.sql.expression for example.

4. support class-bound factory methods on a key class, document them, and have 
them automatically be part of the SQLAlchemy function interface, thereby 
allowing more of the use cases for a particular class to be in one place, see 
https://bitbucket.org/zzzeek/sqlalchemy/src/5659ecb2e8a4aac83a1eb9b2c5ea348f0077ca72/lib/sqlalchemy/sql/elements.py?at=master#cl-2394
 for good examples of this; UnaryExpression provides for about five different 
constructors, so the code can stay with UnaryExpression and the public API is 
just a public_factory() declaration

5. fix the “do I call the function or the __init__?” problem, as public_factory 
rewrites the docs for the __init__ as you see here: 
http://docs.sqlalchemy.org/en/latest/orm/mapping_api.html?highlight=mapper#sqlalchemy.orm.mapper.Mapper.__init__
 the verbiage "This constructor is mirrored as a public API function; see 
mapper() 
<http://docs.sqlalchemy.org/en/latest/orm/mapping_api.html?highlight=mapper#sqlalchemy.orm.mapper>
 for a full usage and argument description.” is automatically generated - 
nobody is confused that this might be the function they’re supposed to be 
calling.   In the source code, Mapper.__init__ is documented fully.

6. I can add new arguments to constructors, like a new Mapper argument, add the 
:paramref: right there below it in the docstring, and not have to worry at all 
about the mapper() function that is elsewhere; it is automatically exported 
with the correct signature and documentation.

7. I can now move all the classes in sqlalchemy.sql.expression to their own 
modules, without any concern that the public “import” space of the 
public-facing function will change, public_factory() allows me to just directly 
give each function the location that I want to display in the documentation.

public_factory() is basically a system to declaratively produce a fixed API 
with fully controllable documentation behavior and zero repetition of verbiage 
on top of a changing set of classes.    


Dmitry Mugtasimov <[email protected]> wrote:

> As I can see from code public_factory() returns a function that instantiate a 
> given class. I can not understand why this can be useful. Why we can't just 
> instantiate class directly as Mapper(...)? Could you please, explain. This is 
> need for preparation of pull request for Spyne (which integrates with 
> SQLAlchemy, but does not support custom mappers).
> 
> mapper = public_factory(Mapper, ".orm.mapper")
> 
> def public_factory(target, location):
>     """Produce a wrapping function for the given cls or classmethod.
> 
>     Rationale here is so that the __init__ method of the
>     class can serve as documentation for the function.
> 
>     """
>     if isinstance(target, type):
>         fn = target.__init__
>         callable_ = target
>         doc = "Construct a new :class:`.%s` object. \n\n"\
>             "This constructor is mirrored as a public API function; see 
> :func:`~%s` "\
>             "for a full usage and argument description." % (
>                 target.__name__, location, )
>     else:
>         fn = callable_ = target
>         doc = "This function is mirrored; see :func:`~%s` "\
>             "for a description of arguments." % location
> 
>     location_name = location.split(".")[-1]
>     spec = compat.inspect_getfullargspec(fn)
>     del spec[0][0]
>     metadata = format_argspec_plus(spec, grouped=False)
>     metadata['name'] = location_name
>     code = """\
> def %(name)s(%(args)s):
>     return cls(%(apply_kw)s)
> """ % metadata
>     env = {'cls': callable_, 'symbol': symbol}
>     exec(code, env)
>     decorated = env[location_name]
>     decorated.__doc__ = fn.__doc__
>     if compat.py2k or hasattr(fn, '__func__'):
>         fn.__func__.__doc__ = doc
>     else:
>         fn.__doc__ = doc
>     return decorated
> 
> -- 
> You received this message because you are subscribed to the Google Groups 
> "sqlalchemy" group.
> To unsubscribe from this group and stop receiving emails from it, send an 
> email to [email protected] 
> <mailto:[email protected]>.
> To post to this group, send email to [email protected] 
> <mailto:[email protected]>.
> Visit this group at http://groups.google.com/group/sqlalchemy 
> <http://groups.google.com/group/sqlalchemy>.
> For more options, visit https://groups.google.com/d/optout 
> <https://groups.google.com/d/optout>.

-- 
You received this message because you are subscribed to the Google Groups 
"sqlalchemy" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to [email protected].
To post to this group, send email to [email protected].
Visit this group at http://groups.google.com/group/sqlalchemy.
For more options, visit https://groups.google.com/d/optout.

Reply via email to