Closed tscrim closed 2 years ago
Replying to @kwankyu:
Replying to @tscrim:
In either case, it would still require some justification for why it is not simply a method of the class. As mentioned, the separation is actually the problem: The programmer should find within the class called the method doing the construction. (Of course, this is not a hard rule as there can be good reasons, typically due to code complexity.)
Code complexity is the justification. The constructor involves all subclasses of the base class. The programmer expects to find the constructor outside of(even in a different file, but not within) one of those classes.
If you were going to construct a FreeResolution
, which returned something where isinstance(res, FreeResolution)
is True
, you would actually be looking at a different place for the construction than in FreeResolution
? This is completely contrary with the behavior of __init__
.
The sole reason to put the constructor inside the class via
__classcall_private__
is to makeisinstance
feature work. You agreed with this. No?
Not with it being the sole reason; just a reason. It also associates the constructor (and any potential normalization of inputs) with the class itself. It also means you do not have to write duplicate documentation or have any question about where certain documentation aspects should go. It also makes it visible by, e.g., FreeResolution??
.
Moreover it is hard to realize that the code defined in the method
__classcall_private__
actually serves as a constructor. The new metaclass is explicit about this.
I agree that the name makes it somewhat difficult to see this without realizing that you have something with a ClasscallMetaclass
for the uninitiated, including what the method __classcall_[private_]_
is suppose to do.
The problems I have with it are:
ClasscallMetaclass
does with a different name.UniqueRepresentation
(both normalization and dispatch)?ClasscallMetaclassInheritComparisons
).Because of the size of the duplication, (1) is a relatively small issue. However, it does have a code smell to me. (2) could be resolved with judicious and meticulous documentation, but I suspect it will just reinforce (1). (3) Right now, the number of classes is small, but it does create lots of extra maintenance burden for future developers (basically technical debt).
Since your biggest issue seems to be discoverability for a programmer with the name (which could potentially also be resolved in cases like this with some code comments, say, just before the class declaration or the __init__()
method), perhaps we can add a feature to ClasscallMetaclass
that also parses in __construction__
(using your experimental name; not 100% set on it yet, possible alternatives __dispatcher__
or __factory__
).
Parent
is unique in the whole sage library. It is irrelevant howParent
solves its own problem.
It is just an example. Right now I don't think we have any others, but this is something that has its uses (although there are likely better methods of doing this; this SO post talks a bit about them). However, what would you expect in such a case? In particular, why is that different than automatic dispatching to a subclass?
Replying to @tscrim:
If you were going to construct a
FreeResolution
, which returned something whereisinstance(res, FreeResolution)
isTrue
, you would actually be looking at a different place for the construction than inFreeResolution
? This is completely contrary with the behavior of__init__
.
By your idiom, we have "constructor + base class" (CB). It seems you see CB primarily as a constructor and I see CB as primarily a base class. This controversy itself indicates CB is a bad idiom. CB is against the principle of separation of concerns.
The sole reason to put the constructor inside the class via
__classcall_private__
is to makeisinstance
feature work. You agreed with this. No?Not with it being the sole reason; just a reason. It also associates the constructor (and any potential normalization of inputs) with the class itself. It also means you do not have to write duplicate documentation or have any question about where certain documentation aspects should go.
I don't see why it is desirable to associate the constructor and the base class by combining the code. There is no duplication in documentations. Actually in (CB), there is no place for either of them.
It also makes it visible by, e.g.,
FreeResolution??
.
Hmm. This is a critical disadvantage of my new metaclass (MC). It will show the documentation of the base class. But this might be fixed.
As I originally suggested, I prefer the classical solution (CL):
def FreeResolution(...): # constructor
...
class FreeResolution_base(...): # base class
....
The sole reason I devised the new metaclass is to make isinstance
feature work. (I think your idiom was first devised for the same purpose. No?) By the way, how important is this feature? This is for end users. I don't really see a use case. Is it important when working with combinatorial objects?
- How do you resolve the issue of who-does-what when it also is, e.g., a
UniqueRepresentation
(both normalization and dispatch)?- It is not scalable: You need a completely parallel tree of classcall metaclass subclass tree (e.g.,
ClasscallMetaclassInheritComparisons
).
Sorry. I don't really understand the issues. A simple-minded answer: do not use MC in problematic situations.
Since your biggest issue seems to be discoverability for a programmer with the name ...
My biggest issue is the presence of the definition of the constructor inside the base class. This looks strange and is hard to understand what is going on with the class. It will be the same when a developer sees it for the first time.
Parent
is unique in the whole sage library. It is irrelevant howParent
solves its own problem.It is just an example. Right now I don't think we have any others, but this is something that has its uses (although there are likely better methods of doing this; this SO post talks a bit about them). However, what would you expect in such a case? In particular, why is that different than automatic dispatching to a subclass?
The difference from what Parent
does with __class__
is that the detailed code of the automatic dispatching (what a constructor does) is all visible inside the base class. (MC) works in the same way with (CB) but moves the dispatching code out of the base class.
Replying to @kwankyu:
Replying to @tscrim:
If you were going to construct a
FreeResolution
, which returned something whereisinstance(res, FreeResolution)
isTrue
, you would actually be looking at a different place for the construction than inFreeResolution
? This is completely contrary with the behavior of__init__
.By your idiom, we have "constructor + base class" (CB). It seems you see CB primarily as a constructor and I see CB as primarily a base class.
No, I see it as both because a base class should have a way to return an instance of itself based upon inputs. Good OOP says you don't care about the implementation details; in this case, the exact class is constructed. Python just doesn't have a good hook for this; __init__
is already working with a specific instance.
This controversy itself indicates CB is a bad idiom.
That is an absurd argument: Any controversy implies bad idiom.
CB is against the principle of separation of concerns.
This makes no sense. It is the concern of the object to construct itself.
From doing a bit of Google searching, the modern trend in a factory design pattern (which is the construction function/method here) when there is a common base case is to associate the factory object to the base class (a C++ example). This is what this idiom is doing with a bit of syntatic sugar to make it work through the usual __call__
protocol (by overriding that special method). We have just chosen a fairly generic name of __classcall__
(that reflects when the method is called) rather than something more specific like __construction__
or _factory_
.
The sole reason to put the constructor inside the class via
__classcall_private__
is to makeisinstance
feature work. You agreed with this. No?Not with it being the sole reason; just a reason. It also associates the constructor (and any potential normalization of inputs) with the class itself. It also means you do not have to write duplicate documentation or have any question about where certain documentation aspects should go.
I don't see why it is desirable to associate the constructor and the base class by combining the code. There is no duplication in documentations.
Yes there is. If we take (CL) remember that the forward facing documentation is in the constructor function. So FreeResolution?
gives that documentation. However, if we do the same for res?
, we get the class documentation of FreeResolution_base
. Thus, where should the description of the object go?
Another way to think of this, which is what I am getting at with overriding __class__
, is that it is an upscale version of __init__
of the class. We at least agree that __init__
should be defined in the class, right?
Remember that subclasses are a very useful way of getting around having a bunch of if
statements in the code deciding behavior of objects (and generally reduces the maintenance burden). This is basically why we have the two different classes for the Singular version and the free module version (or if we come along with a different implementation later on). They are implementation details that do not change the general model the code is representing, and associating the constructor with the base class is in line with that implementation too.
(I actually thought about splitting the resolution code up based upon the input being a module/ideal/matrix. However, I concluded that the tradeoff in complexity for doing that wasn't worth it.)
Actually in (CB), there is no place for either of them.
All documentation about the object should clearly go in the class level description of the base class. Furthermore, if you need to document specific inputs to the base class, that can go in the __init__
documentation, which is the natural place to look as it is describing its inputs.
It also makes it visible by, e.g.,
FreeResolution??
.Hmm. This is a critical disadvantage of my new metaclass (MC). It will show the documentation of the base class. But this might be fixed.
I am just guessing about your possible solutions to this, but the top one would be to copy the base class's doc. However, you would also have to deal with the raw code (where there would be a disassociation with the documentation), doctests (how would I associate a particular doctest with the constructor versus the base class), and how we measure coverage (related also with how many duplicate tests would this yield). Although I probably shouldn't comment too much until there is an actual proposal.
As I originally suggested, I prefer the classical solution (CL): The sole reason I devised the new metaclass is to make
isinstance
feature work. (I think your idiom was first devised for the same purpose. No?)
AFAIK, the ClasscallMetaclass
was originally for normalizing inputs to CachedRepresentation
. Although it was soon applied to handle construction (and possibly dispatch) of element classes, such as Tableau
, without having to construct an explicit parent. This was mostly used for combinatorial objects early on.
By the way, how important is this feature? This is for end users. I don't really see a use case. Is it important when working with combinatorial objects?
It is also useful for things like algebras that have different implementations based upon their inputs. Polynomial rings are a great example of this (but don't currently implement this idiom). For instance, you could have code like
def foo(R=None):
if R is None:
R = PolynomialRing(QQ, 'x,y')
if isinstance(R, PolynomialRing_general):
return 1
return 2
From a naïve look, why would the default argument for R
lead to case 1
? It seems like they should be different. By being separate, the constructor is not under even a tacit promise to return a subclass of PolynomialRing_general
. In fact, this is true in this example as there is no common ABC for univariate and multivariate polynomial rings (which has caused me some headaches).
You also have to rename something when there is a conflict between the name of the base class and the factory name:
sage: import_statements(matrix)
# ** Warning **: several names for that object: Matrix, matrix
from sage.matrix.constructor import Matrix
sage: from sage.structure.element import Matrix # not the constructor!
Hence, it would make it easier on the developer who has to use the class.
- How do you resolve the issue of who-does-what when it also is, e.g., a
UniqueRepresentation
(both normalization and dispatch)?- It is not scalable: You need a completely parallel tree of classcall metaclass subclass tree (e.g.,
ClasscallMetaclassInheritComparisons
).Sorry. I don't really understand the issues. A simple-minded answer: do not use MC in problematic situations.
Please define what you mean by a problematic situation. It seems like you are wanting to say it is a case when there is a conflict between your new metaclass and UniqueRepresentation
(or anything else that uses one of the other metaclasses). This is not a feasible option as we need to support both behaviors. Your proposal is creating issues where there were none previously.
Since your biggest issue seems to be discoverability for a programmer with the name ...
My biggest issue is the presence of the definition of the constructor inside the base class. This looks strange and is hard to understand what is going on with the class. It will be the same when a developer sees it for the first time.
That is true for basically any idiom, especially the project-specific ones. (As I mentioned above, this is not so Sage specific, mostly just the name __classcall_private__
.) For instance, UniqueRepresentation
. I agree that a new Sage developer who is an experienced programmer will have to learn something; there is no perfect solution. However, this idiom is easy to teach, simple, flexible, and has many natural aspects.
Parent
is unique in the whole sage library. It is irrelevant howParent
solves its own problem.It is just an example. Right now I don't think we have any others, but this is something that has its uses (although there are likely better methods of doing this; this SO post talks a bit about them). However, what would you expect in such a case? In particular, why is that different than automatic dispatching to a subclass?
The difference from what
Parent
does with__class__
is that the detailed code of the automatic dispatching (what a constructor does) is all visible inside the base class. (MC) works in the same way with (CB) but moves the dispatching code out of the base class.
I don't understand this. The base class is the one that chooses which instance to build and ultimately dispatches out to that __init__
. There is no initialization or construction dispatching done with Parent
setting __class__
to a dynamic subclass built from the category (in particular, it never goes back down or calls any other __init__
lower that Parent
in its MRO). In fact, that the constructor code becoming visible somewhere in the MRO with the (CB) method is what you want, right?
Replying to @tscrim:
because a base class should have a way to return an instance of itself based upon inputs.
But not of its subclass. It looks like that in your idiom.
Sorry. I feel tired of this discussion. I can't follow your arguments and I don't want to repeat my arguments.
As least, your idiom works. My complaint is only about the organization of the code. I will revert back the branch to your idiom.
Branch pushed to git repo; I updated commit sha1. New commits:
cd0b948 | Revert to `__classcall_private__` idiom |
d0e11a3 | Some edits |
22dd075 | Nonhomogeneous examples |
e5be7e4 | Merge branch 'develop' |
22f74a2 | Remove ConstructorBaseclassMetaclass |
609b40d | Recover ClasscallMetaclass |
b939bbc | Fix in projective_morphism.py |
1622605 | Fix of _repr_() |
81d6afb | Recover errorneously deleted constant_function.pyx |
d8bc44f | Minor fixes |
Mostly minor edits. I modified or deleted some examples of graded free resolutions with non-homogeneous inputs.
Otherwise looks good to me.
Reviewer: Kwankyu Lee
Thank you.
The only thing I am not sure about is the change for the resolution in free module handling things rather than FreeResolution
. This also includes my implementation for the multipolynomial ideals (I think at the time, I didn’t have a single such entry point). We are duplicating some of the logic there, so there is more work to be done if another implementation is provided (in particular, if it expands the capabilities).
This isn’t really a big thing as we can revisit it should additional implementations be provided. I just wanted to see what you thought about this.
Replying to @tscrim:
Thank you.
The only thing I am not sure about is the change for the resolution in free module handling things rather than
FreeResolution
.
Like your M.graded_free_resolution()
, the dispatching to specific free resolution subclasses (those with _singular
and _free_module
postfixes) should happen in M.free_resolution()
as this method has information about M
. I regard these methods as the primary dispatcher to the free resolution subclasses.
Then there is your FreeResolution
dispatcher (I will not use the term "constructor" as, it seems, we understand the term differently) inside FreeResolution
base class. I regard this as a secondary way to use the free resolution subclasses. Hence your dispatcher may use the methods M.[graded_]free_resolution()
depending on the type of the input M
. Then there would be no duplication of code.
... my implementation for the multipolynomial ideals (I think at the time, I didn’t have a single such entry point).
I don't understand this. Would you name it explicitly? Where is it?
Replying to @kwankyu:
Replying to @tscrim:
Thank you.
The only thing I am not sure about is the change for the resolution in free module handling things rather than
FreeResolution
.Like your
M.graded_free_resolution()
, the dispatching to specific free resolution subclasses (those with_singular
and_free_module
postfixes) should happen inM.free_resolution()
as this method has information aboutM
. I regard these methods as the primary dispatcher to the free resolution subclasses.
Your current proposal is not a good programming practice as it is not scalable. More concretely, you now have at least two different ways (that are not linked) for creating the free resolutions from, e.g., a module. Now you need to keep them consistent, which can be tedious as the code grows.
Then there is your
FreeResolution
dispatcher (I will not use the term "constructor" as, it seems, we understand the term differently) insideFreeResolution
base class. I regard this as a secondary way to use the free resolution subclasses. Hence your dispatcher may use the methodsM.[graded_]free_resolution()
depending on the type of the inputM
. Then there would be no duplication of code.
This would be good with me. So we would have
def __classcall_private__(cls, M, *args, **kwds):
try:
return M.free_resolution(*args, **kwds)
except AttributeError:
raise ValueError(“unable to construct a free resolution”)
It is just right now we are duplicating the code. You will also need to add a [graded_]free_resolution()
method to matrices.
The reason I didn’t chose this approach was because there would still be some code duplication and scalability to address since all of these inputs types are treated as a module. For instance, if I implement a way to compute resolutions using a polynomial ring over an arbitrary field (not just ones that Singular can recognize). Then we need to update all of the different free resolution methods to allow for this more general case. However your proposal is reasonable and isn’t (yet) so unwieldy that it is a significant burden, so let’s go with it.
... my implementation for the multipolynomial ideals (I think at the time, I didn’t have a single such entry point).
I don't understand this. Would you name it explicitly? Where is it?
Clearly the main entry point was designed to be FreeResolution
.
Replying to @tscrim:
You will also need to add a
[graded_]free_resolution()
method to matrices.
Free resolutions are about modules (ideals are also modules). Matrix is one way to specify a module. You don't need to attach the methods to matrices. I would not.
Replying to @kwankyu:
Replying to @tscrim:
You will also need to add a
[graded_]free_resolution()
method to matrices.Free resolutions are about modules (ideals are also modules). Matrix is one way to specify a module. You don't need to attach the methods to matrices. I would not.
Then we should disallow a matrix as input altogether.
Replying to @tscrim:
Replying to @kwankyu:
Replying to @tscrim:
You will also need to add a
[graded_]free_resolution()
method to matrices.Free resolutions are about modules (ideals are also modules). Matrix is one way to specify a module. You don't need to attach the methods to matrices. I would not.
Then we should disallow a matrix as input altogether.
You may do so for FreeResolution
dispatcher. But please do not remove it from the free resolution classes for internal use. By allowing a matrix as input to our free resolution classes, we may remove a small amount of conversion overhead (matrix -> module -> matrix) in those not-uncommon cases that we have a matrix on our hand.
I think you’re trying to have it both ways with matrices being automatically being treated as submodules (for an insignificant optimization (relative to the main computations) at the cost of code complexity). However, what I will do is have special handling within the FreeResolution
hook because being consistent across the inputs is important to me and I want to do what I can to accommodate your viewpoints.
Replying to @tscrim:
I think you’re trying to have it both ways with matrices being automatically being treated as submodules (for an insignificant optimization (relative to the main computations)
Yes.
at the cost of code complexity.
As I see it, the code complexity is also insignificant, and the code is already there.
what I will do is have special handling within the
FreeResolution
hook because being consistent across the inputs is important to me.
Okay. Then I won't insist on the matrix input if, as you see it, it would be a maintenance burden for your work with free resolution classes.
Are you working on this or waiting for review as it is?
Sorry, I have been busy with family stuff and writing grants, and I did not have access to my laptop. Right now Sage is building on my laptop, and I plan to make the change after that is done. Give me a few more hours.
Here we go. I did was we agreed upon: the FreeResolution
dispatches out to [graded_]free_resolution()
with special handling for matrices.
I changed the projective morphism to use the hook.
I also fixed some an issue with the previous ticket that I should have caught: the singular.pyx
has full coverage now.
Branch pushed to git repo; I updated commit sha1. New commits:
250c5cb | Some edits |
In FreeModule
, why do you check if the module
is a free module only when module
is a matrix? You wanted to provide free resolutions for free modules.
In sage.rings.ideal
, you provided free_resolution()
hook. In this context, an ideal is of a general commutative ring, but your code seems to assume that the ring is a univariate polynomial ring over a field. You need to check this first. If the ring is other than that, raise a NotImplemented
error.
Replying to Kwankyu Lee:
In
FreeModule
, why do you check if themodule
is a free module only whenmodule
is a matrix? You wanted to provide free resolutions for free modules.
This is what we agreed on: special check for matrices because you wanted the code to handle it but not add a method to the matrix classes. Thus, the main entry point needs to process it. Everything else is delegated to free_resolution()
.
In
sage.rings.ideal
, you providedfree_resolution()
hook. In this context, an ideal is of a general commutative ring, but your code seems to assume that the ring is a univariate polynomial ring over a field. You need to check this first. If the ring is other than that, raise aNotImplemented
error.
I require the ideal is principal, which is automatically free because it has a single generator. This case holds for all commutative rings, right? All other cases will raise an NotImplementedError
either via the is_principal()
call or from the result being False
.
Replying to Travis Scrimshaw:
In
sage.rings.ideal
, you providedfree_resolution()
hook. In this context, an ideal is of a general commutative ring, but your code seems to assume that the ring is a univariate polynomial ring over a field. You need to check this first. If the ring is other than that, raise aNotImplemented
error.I require the ideal is principal, which is automatically free because it has a single generator. This case holds for all commutative rings, right?
Yes.
All other cases will raise an
NotImplementedError
either via theis_principal()
call or from the result beingFalse
.
I see. Okay.
If you’re good with my changes and the patchbot comes back green (or you are confident with the appropriate tests being run), then positive review?
Replying to Travis Scrimshaw:
Replying to Kwankyu Lee:
In
FreeModule
, why do you check if themodule
is a free module only whenmodule
is a matrix? You wanted to provide free resolutions for free modules.This is what we agreed on: special check for matrices because you wanted the code to handle it
No. I wanted that the matrix input is kept in (__init__()
of) free resolution subclasses.
But I even agreed to remove it if you want because of maintenance burden.
For FreeResolution
dispatcher, I think that not accepting matrix input is the right way. I think I agreed upon this...
You removed
if isinstance(module, Ideal_generic):
S = module.ring()
if len(module.gens()) == 1 and S in IntegralDomains():
is_free_module = True
elif isinstance(module, Module_free_ambient):
S = module.base_ring()
if (S in PrincipalIdealDomains()
or isinstance(module, FreeModule_generic)):
is_free_module = True
from FreeResolution
dispatcher, are these cases handled in free_resolution()
? Or you decided not to handle those cases?
Replying to Kwankyu Lee:
Replying to Travis Scrimshaw:
Replying to Kwankyu Lee:
In
FreeModule
, why do you check if themodule
is a free module only whenmodule
is a matrix? You wanted to provide free resolutions for free modules.This is what we agreed on: special check for matrices because you wanted the code to handle it
No. I wanted that the matrix input is kept in (
__init__()
of) free resolution subclasses. But I even agreed to remove it if you want because of maintenance burden.
This isn't really that much of a burden, especially since we don't fundamentally want to work with the module but the underlying matrix. We have a good justification for the special case of matrices.
For
FreeResolution
dispatcher, I think that not accepting matrix input is the right way. I think I agreed upon this...
If the class accepts a matrix, then the dispatcher should be able to handle the matrix. I think it would be really strange to have this discrepancy. Plus the constructor does some things to sanitize the input; in particular, it makes sure the matrix is immutable as this can lead to some very subtle bugs that are very hard to track down. (For example, you pass a mutable matrix into the constructor, compute the resolution, mutate the matrix; now the resolution does not necessarily match the input matrix.)
Actually, this makes me think of a case it should also handle: a free resolution input (where it would just return the input up to maybe stripping/adding the grading).
Replying to Kwankyu Lee:
You removed
if isinstance(module, Ideal_generic): S = module.ring() if len(module.gens()) == 1 and S in IntegralDomains(): is_free_module = True elif isinstance(module, Module_free_ambient): S = module.base_ring() if (S in PrincipalIdealDomains() or isinstance(module, FreeModule_generic)): is_free_module = True
from
FreeResolution
dispatcher, are these cases handled infree_resolution()
? Or you decided not to handle those cases?
These are handled by the respective free_resolution()
methods in, e.g., ideal.
Replying to Travis Scrimshaw:
Actually, this makes me think of a case it should also handle: a free resolution input (where it would just return the input up to maybe stripping/adding the grading).
Why? This is strange. PolynomialRing(PolynomialRing(QQ))
does not work in that way.
trac is buggy...
Replying to Kwankyu Lee:
Replying to Travis Scrimshaw:
Actually, this makes me think of a case it should also handle: a free resolution input (where it would just return the input up to maybe stripping/adding the grading).
Why? This is strange.
PolynomialRing(PolynomialRing(QQ))
does not work in that way.
Right, that was dumb of me. I was thinking it was more Element
-like (e.g., Partition
), but it is Parent
-like (or function-like).
Do you think it would be useful to implement a method in graded free resolutions like as_ungraded()
that forgets the grading?
Replying to Travis Scrimshaw:
Do you think it would be useful to implement a method in graded free resolutions like
as_ungraded()
that forgets the grading?
No as far as I know. Let's leave that as a future work for who needs it.
Now let this go.
Branch pushed to git repo; I updated commit sha1 and set ticket back to needs_review. New commits:
6dba5e5 | Hot fixes |
Thank you. This improved the code from my original proposal.
It took me a minute to realize that this is equivalent to the way I am used to seeing the definition of a resolution with 0 <- M <- F_1 <- ...
. However, this is definitely the better way to present this given the string representation.
Replying to Travis Scrimshaw:
Thank you. This improved the code from my original proposal.
Thank you for the work.
If we want to be more mathematical, matrix input may be entirely removed. But we may regard matrix input as an added conveniency feature. I am happy in either way.
It took me a minute to realize that this is equivalent to the way I am used to seeing the definition of a resolution with
0 <- M <- F_1 <- ...
. However, this is definitely the better way to present this given the string representation.
A resolution of M
is also said to be a resolution of F_1/M
. See pages 22-23 of
https://faculty.math.illinois.edu/Macaulay2/Book/ComputationsBook/book/book.pdf
It seems that "resolution of M
" is more conventional, but I think "resolution of F_1/M
" is more reasonable. If you want to augment F_1 <- ... <- 0
at the left to get an exact sequence, that should be
0 <- F_1/M <- F_1 <- ... <- 0
where the canonical map F_1/M <- F_1
is our 0-th differential map and F_1/M
is our target()
.
Changed branch from public/rings/free_gr_res_hook-34379 to 6dba5e5
In #33950, free (graded) resolutions for modules over polynomial rings were added. However, one needs to import a top-level function to do the construction. The goal of this ticket is to add a method, such as
free_resolution()
, to these (sub)modules (and ideals) to ease access.Depends on #33950
CC: @kwankyu
Component: user interface
Author: Travis Scrimshaw
Branch/Commit:
6dba5e5
Reviewer: Kwankyu Lee
Issue created by migration from https://trac.sagemath.org/ticket/34379