Skip to content

Commit

Permalink
Trac #18174: Generalize base class construction for functorial constr…
Browse files Browse the repository at this point in the history
…uction categories

This ticket implements the following syntactic sugar for functorial
construction categories::
{{{
    GradedModules(K)  ->  Modules(K).Graded()
    Modules.Graded(K) ->  Modules(K).Graded()
}}}
This makes them behave consistently with `CategoryWithAxiom`.

As is noted in the code, the internal logic is very similar, but there
seemed at this point to be no good way to avoid the duplication.

The first syntactic sugar was actually already partially implemented
for `GradedModulesCategory`, so half of the work is just generalizing
existing code.

The syntactic sugar is not valid for construction categories that take
extra arguments like `Algebras`, since there is no generic way to
decide what the argument should be used for:
{{{
    Sets.Algebras(K)  ->   Sets(K).Algebras() or Sets().Algebras(K) ?
}}}

URL: http://trac.sagemath.org/18174
Reported by: tscrim
Ticket author(s): Travis Scrimshaw
Reviewer(s): Nicolas Thiéry
  • Loading branch information
Release Manager authored and vbraun committed Apr 15, 2015
2 parents 77e14a9 + c35a270 commit 3b669b7
Show file tree
Hide file tree
Showing 3 changed files with 135 additions and 75 deletions.
135 changes: 129 additions & 6 deletions src/sage/categories/covariant_functorial_construction.py
Expand Up @@ -42,9 +42,12 @@
# http://www.gnu.org/licenses/
#******************************************************************************
from sage.misc.cachefunc import cached_function, cached_method
from sage.misc.lazy_attribute import lazy_class_attribute
from sage.misc.lazy_import import LazyImport
from sage.categories.category import Category
from sage.structure.sage_object import SageObject
from sage.structure.unique_representation import UniqueRepresentation
from sage.structure.dynamic_class import DynamicMetaclass

class CovariantFunctorialConstruction(UniqueRepresentation, SageObject):
r"""
Expand Down Expand Up @@ -225,26 +228,145 @@ class FunctorialConstructionCategory(Category): # Should this be CategoryWithBas
functorial construction
"""

@lazy_class_attribute
def _base_category_class(cls):
"""
Recover the class of the base category.
OUTPUT:
A *tuple* whose single entry is the base category class.
.. WARNING::
This is only used for functorial construction categories
that are not implemented as nested classes, and won't work
otherwise.
.. SEEALSO:: :meth:`__classcall__`
EXAMPLES::
sage: GradedModules._base_category_class
(<class 'sage.categories.modules.Modules'>,)
sage: GradedAlgebrasWithBasis._base_category_class
(<class 'sage.categories.algebras_with_basis.AlgebrasWithBasis'>,)
The reason for wrapping the base category class in a tuple is
that, often, the base category class implements a
:meth:`__classget__` method which would get in the way upon
attribute access::
sage: F = GradedAlgebrasWithBasis
sage: F._foo = F._base_category_class[0]
sage: F._foo
Traceback (most recent call last):
...
ValueError: could not infer axiom for the nested class
<...AlgebrasWithBasis'> of <...GradedAlgebrasWithBasis'>
.. TODO::
The logic is very similar to that implemented in
:class:`CategoryWithAxiom._base_category_class`. Find a
way to refactor this to avoid the duplication.
"""
module_name = cls.__module__.replace(cls._functor_category.lower() + "_","")
import sys
name = cls.__name__.replace(cls._functor_category, "")
__import__(module_name)
module = sys.modules[module_name]
return (module.__dict__[name],)

@staticmethod
def __classcall__(cls, category=None, *args):
"""
Make ``XXXCat(**)`` a shorthand for ``Cat(**).XXX()``.
EXAMPLES::
sage: GradedModules(ZZ) # indirect doctest
Category of graded modules over Integer Ring
sage: Modules(ZZ).Graded()
Category of graded modules over Integer Ring
sage: Modules.Graded(ZZ)
Category of graded modules over Integer Ring
sage: GradedModules(ZZ) is Modules(ZZ).Graded()
True
.. SEEALSO:: :meth:`_base_category_class`
.. TODO::
The logic is very similar to that implemented in
:class:`CategoryWithAxiom.__classcall__`. Find a way to
refactor this to avoid the duplication.
"""
base_category_class = cls._base_category_class[0]
if isinstance(category, base_category_class):
return super(FunctorialConstructionCategory, cls).__classcall__(cls, category, *args)
else:
return cls.category_of(base_category_class(category, *args))

@staticmethod
def __classget__(cls, category, owner):
def __classget__(cls, base_category, base_category_class):
r"""
Special binding for covariant constructions
Special binding for covariant constructions.
This implements a hack allowing e.g. ``category.Subquotients``
to recover the default ``Subquotients`` method defined in
``Category``, even if it has been overriden by a
``Subquotients`` class.
TESTS::
EXAMPLES::
sage: Sets.Subquotients
<class 'sage.categories.sets_cat.Sets.Subquotients'>
sage: Sets().Subquotients
Cached version of <function Subquotients at ...>
This method also initializes the attribute
``_base_category_class`` if not already set::
sage: Sets.Subquotients._base_category_class
(<class 'sage.categories.sets_cat.Sets'>,)
It also forces the resolution of lazy imports (see :trac:`15648`)::
sage: type(Algebras.__dict__["Graded"])
<type 'sage.misc.lazy_import.LazyImport'>
sage: Algebras.Graded
<class 'sage.categories.graded_algebras.GradedAlgebras'>
sage: type(Algebras.__dict__["Graded"])
<type 'sage.misc.classcall_metaclass.ClasscallMetaclass'>
.. TODO::
The logic is very similar to that implemented in
:class:`CategoryWithAxiom.__classget__`. Find a way to
refactor this to avoid the duplication.
"""
if category is None:
if base_category is not None:
assert base_category.__class__ is base_category_class
assert isinstance(base_category_class, DynamicMetaclass)
if isinstance(base_category_class, DynamicMetaclass):
base_category_class = base_category_class.__base__
if "_base_category_class" not in cls.__dict__:
cls._base_category_class = (base_category_class,)
else:
assert cls._base_category_class[0] is base_category_class, \
"base category class for {} mismatch; expected {}, got {}".format(
cls, cls._base_category_class[0], base_category_class)

# Workaround #15648: if Sets.Subquotients is a LazyImport object,
# this forces the substitution of the object back into Sets
# to avoid resolving the lazy import over and over
if isinstance(base_category_class.__dict__[cls._functor_category], LazyImport):
setattr(base_category_class, cls._functor_category, cls)
if base_category is None:
return cls
return getattr(super(category.__class__.__base__, category), cls._functor_category)
return getattr(super(base_category.__class__.__base__, base_category),
cls._functor_category)

@classmethod
@cached_function
Expand Down Expand Up @@ -286,7 +408,8 @@ def __init__(self, category, *args):
sage: from sage.categories.covariant_functorial_construction import CovariantConstructionCategory
sage: class FooBars(CovariantConstructionCategory):
... _functor_category = "FooBars"
....: _functor_category = "FooBars"
....: _base_category_class = (Category,)
sage: Category.FooBars = lambda self: FooBars.category_of(self)
sage: C = FooBars(ModulesWithBasis(ZZ))
sage: C
Expand Down
74 changes: 5 additions & 69 deletions src/sage/categories/graded_modules.py
Expand Up @@ -33,83 +33,19 @@ def __init__(self, base_category):
Rational Field
sage: GradedHopfAlgebrasWithBasis(QQ).base_ring()
Rational Field
"""
super(GradedModulesCategory, self).__init__(base_category, base_category.base_ring())

_functor_category = "Graded"

@lazy_class_attribute
def _base_category_class(cls):
"""
Recover the class of the base category.
OUTPUT:
A *tuple* whose first entry is the base category class.
TESTS::
.. WARNING::
This is only used for graded categories that are not
implemented as nested classes, and won't work otherwise.
.. SEEALSO:: :meth:`__classcall__`
EXAMPLES::
sage: GradedModules._base_category_class
(<class 'sage.categories.modules.Modules'>,)
sage: GradedAlgebrasWithBasis._base_category_class
(<class 'sage.categories.algebras_with_basis.AlgebrasWithBasis'>,)
The reason for wrapping the base category class in a tuple is
that, often, the base category class implements a
:meth:`__classget__` method which would get in the way upon
attribute access::
sage: F = GradedAlgebrasWithBasis
sage: F._foo = F._base_category_class[0]
sage: F._foo
Traceback (most recent call last):
...
AssertionError: base category class for <...AlgebrasWithBasis'> mismatch;
expected <...Algebras'>, got <...GradedAlgebrasWithBasis'>
"""
module_name = cls.__module__.replace("graded_","")
import sys
name = cls.__name__.replace("Graded","")
__import__(module_name)
module = sys.modules[module_name]
return (module.__dict__[name],)

@staticmethod
def __classcall__(cls, category, *args):
"""
Magic support for putting Graded categories in their own file.
EXAMPLES::
sage: GradedModules(ZZ) # indirect doctest
sage: GradedModules(ZZ)
Category of graded modules over Integer Ring
sage: Modules(ZZ).Graded()
Category of graded modules over Integer Ring
sage: GradedModules(ZZ) is Modules(ZZ).Graded()
True
.. TODO::
Generalize this support for all other functorial
constructions if at some point we have a category ``Blah`` for
which we want to implement the construction ``Blah.Foo`` in a
separate file like we do for e.g. :class:`GradedModules`,
:class:`GradedAlgebras`, ...
.. SEEALSO:: :meth:`_base_category_class`
"""
base_category_class = cls._base_category_class[0]
if isinstance(category, base_category_class):
return super(GradedModulesCategory, cls).__classcall__(cls, category, *args)
else:
return base_category_class(category, *args).Graded()
super(GradedModulesCategory, self).__init__(base_category, base_category.base_ring())

_functor_category = "Graded"

def _repr_object_names(self):
"""
Expand Down
1 change: 1 addition & 0 deletions src/sage/categories/homsets.py
Expand Up @@ -182,6 +182,7 @@ class HomsetsOf(HomsetsCategory):
sage: TestSuite(C).run(skip=['_test_category_graph'])
sage: TestSuite(C).run()
"""
_base_category_class = (Category,)

def _repr_object_names(self):
"""
Expand Down

0 comments on commit 3b669b7

Please sign in to comment.