-
Notifications
You must be signed in to change notification settings - Fork 218
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat!: collapse extraneous XBlock mixins #718
Conversation
9c06865
to
21ce2ab
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Notes for reviewers 👀
@@ -128,6 +128,7 @@ | |||
('py:class', 'aside'), | |||
('py:class', 'aside_fn'), | |||
('py:class', 'webob.Request'), | |||
('py:class', 'webob.Response'), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added so that we can reference the "Response" class in a new docstring.
class XBlockMixin(xblock.core.XBlockMixin): | ||
""" | ||
A wrapper around xblock.core.XBlockMixin that provides backwards compatibility for the old location. | ||
|
||
Deprecated. | ||
""" | ||
def __init__(self, *args, **kwargs): | ||
warnings.warn("Please use xblock.core.XBlockMixin", DeprecationWarning, stacklevel=2) | ||
super().__init__(*args, **kwargs) | ||
|
||
|
||
# For backwards compatibility, provide the XBlockMixin in xblock.fields | ||
# without causing a circular import | ||
xblock.fields.XBlockMixin = XBlockMixin |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We're just removing the wrapper locations, which have been deprecated since 2014. XBlockMixin is still available at xblock.core.XBlockMixin.
# OrderedDict is used so that namespace attributes are put in predictable order | ||
# This allows for simple string equality assertions in tests and have no other effects | ||
XML_NAMESPACES = OrderedDict([ | ||
("option", "http://code.edx.org/xblock/option"), | ||
("block", "http://code.edx.org/xblock/block"), | ||
]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Comment copied in from xblock.mixins
|
||
# __all__ controls what classes end up in the docs. | ||
__all__ = ['XBlock'] | ||
__all__ = ['XBlock', 'XBlockAside'] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This puts XBlockAside in the docs
UNSET = object() | ||
|
||
|
||
class XBlockMixin(ScopedStorageMixin): | ||
class _AutoNamedFieldsMetaclass(type): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
xblock.internal.NamedAttributesMetaclass was responsible for sticking __name__
attributes on any Nameable
objects defined on its classes. In practice, Field was the only type of Nameable attribute.
_AutoNamedFieldsMetaclass is essentially the same as NamedAttributesMetaclass, but to simplify things, it only just operates on Fields.
_AutoNamedFieldsMetaclass continues to be the metaclass for both XBlock and XBlockAside (although XBlock further specializes it as _HasChildrenMetaclass, defined further down).
super().__init__(runtime=runtime, scope_ids=scope_ids, field_data=field_data, *args, **kwargs) | ||
|
||
@property | ||
def usage_key(self): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
usage_key and context_key are moved to Blocklike so that XBlockAsides can use them too.
@staticmethod | ||
def needs_name(field): | ||
""" | ||
Returns whether the given ) is yet to be named. | ||
""" | ||
return not field.__name__ | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was copied in from Nameable, but I recall now that I've refactored _AutoNameFieldsMetaclass such that this is now unused. I'll remove it before merging.
@staticmethod | |
def needs_name(field): | |
""" | |
Returns whether the given ) is yet to be named. | |
""" | |
return not field.__name__ |
@@ -28,42 +27,3 @@ def __get__(self, instance, owner): | |||
|
|||
|
|||
class_lazy = LazyClassProperty # pylint: disable=invalid-name | |||
|
|||
|
|||
class NamedAttributesMetaclass(type): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
NamedAttributesMetaclass and Nameable have been de-generalized into xblock.core._AutoNamedFieldsMetaclass and xblock.fields.Field, respectively.
xblock/test/test_core.py
Outdated
@Blocklike.needs("field-data") | ||
class FieldTester(Blocklike): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
RuntimeServicesMixin defined the @needs
decorator, which allowed its subclass ScopedStorageMixin to invoke @needs("field-data")
.
However, now Blocklike defines @needs
, so Blocklike itself cannot invoke @needs("field-data")
.
Thus, XBlock and XBlockAside now each invoke @needs("field-data")
individually, and thus tests that subclass Blocklike must also invoke @needs("field-data")
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you manually invoke the decorator on Blocklike
after it's defined? I haven't done something like this in ages, but I think it'd be something like this?
class Blocklike:
# lots of stuff here
Blocklike = Blocklike.needs(Blocklike, "field-data")
patched_warn.assert_called_once_with("XBlock %s does not contain field %s", type(block), parameter_name) | ||
patched_warn.assert_called_once_with("%s does not contain field %s", type(block), parameter_name) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This warning was incorrect when the object was an XBlockAside.
I considered changing it to "Blocklike", but then the message would be:
Blocklike FooBlock does not contain field bar
which isn't any better than:
FooBlock does not contain field bar
.
I could be convinced otherwise, esp if folks feel that the former is more regex-able.
91c528d
to
b977536
Compare
cca0fe4
to
add3c0e
Compare
Various extraneous classes have been removed from the XBlock API, simplifying its implementation and making debugging of XBlock instances easier. We believe that most, if not all, XBlock API users will be unaffected by this change. Some improvements have also been made to the reference documentation. See CHANGELOG.rst for details. Closes: openedx#714
add3c0e
to
51b0920
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just some comments/questions. No major concerns.
xblock/core.py
Outdated
(...or don't do that, because it's confusing for other developers, and linters/type-checkers | ||
won't understand that the mixins are part of XBlock instances, so your code will be full | ||
of ignore statements :) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I appreciate the sentiment here, but this statement makes it sound like there's a different, recommended course of action, which should be elaborated on if it's the case.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@ormsbee You know, on second thought, what I really want is for people to avoid adding new xblock mixins to the edx-platform runtime. If people want to add new xblock mixins to their custom fork or custom runtime, that's A-OK. I'll move my big DO-NOT-USE-THIS comment to edx-platform instead.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
node.set(field_name, text_value) | ||
|
||
|
||
class XBlockMixin(Blocklike): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If XBlockMixin
is a subclass of Blocklike
that overrides no other behavior, why not rename Blocklike
to XBlockMixin
? That would be more in line with our conventions, wouldn't it (where XBlock
and XBlockAside
both inherit from XBlockMixin
)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So XBlockMixin is an unfortunate name. "Mixin" in the traditional sense means a class that isn't a meaningful base in itself, but is to be "mixed in" to other classes to add extra functionality. In that sense, the XBlockMixin isn't a mixin; rather, it's supposed to be a base class for new custom XBlock mixins.
Contrast this with Blocklike, which is a meaningful base, one which I expect to be used all over xblock and edx-platform as a type annotation:
def this_function_could_take_a_block_or_an_aisde(block: Blocklike):
...
...which I think reads better than using XBlockMixin as the annotation:
def this_function_could_take_a_block_or_an_aisde(block: XBlockMixin):
...
I guess we could still reconcile the two classes by just aliasing XBlockMixin = Blocklike
... what do you think @ormsbee ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay, you've convinced me. Based on your description, XBlockBase
would also work for me, but I'm good with Blocklike
. I do like the idea of the alias since I have no idea if someone might have built off of that in their fork/plugin.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@ormsbee On second thought, I'm actually inclined to leave XBlockMixin as its own subclass of Blocklike. It can't put my finger on exactly why, but it seems like it'd be less surprising to have XBlockMixin be its own class rather than an alias. Like you say, XBlockMixin is part of the public API, so we can't just delete it.
xblock/test/test_core.py
Outdated
@Blocklike.needs("field-data") | ||
class FieldTester(Blocklike): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you manually invoke the decorator on Blocklike
after it's defined? I haven't done something like this in ages, but I think it'd be something like this?
class Blocklike:
# lots of stuff here
Blocklike = Blocklike.needs(Blocklike, "field-data")
Good idea. That bit of backwards incompatibility was making me a little nervous, so I'm glad to smooth it out. The syntax ended up being: Blocklike.needs('field-data')(Blocklike) |
Supporting info
Proposed in this DEPR issue:
excerpt:
The edx-platform upgrade PR is here:
Description
Various extraneous classes have been removed from the XBlock API. We believe that most, if not all, XBlock API users will be unaffected by this change.
Diagram
Generated with
pyreverse
anddot
.Before
After
Change list
xblock.XBlockMixin
(still available asxblock.core.XBlockMixin
)xblock.core.SharedBlockBase
(replaced withxblock.core.Blocklike
)xblock.internal.Nameable
xblock.internal.NamedAttributesMetaclass
xblock.django.request.HeadersDict
xblock.fields.XBlockMixin
(still available asxblock.core.XBlockMixin
)xblock.mixins.RuntimeServicesMixin
xblock.mixins.ScopedStorageMixin
xblock.mixins.IndexInfoMixin
xblock.mixins.XmlSerializationMixin
xblock.mixins.HandlersMixin
xblock.mixins.ChildrenModelMetaclass
xblock.mixins.HierarchyMixin
xblock.mixins.ViewsMixin
xblock.core.Blocklike
, the new common ancestor ofXBlock
andXBlockAside
, andXBlockMixin
, replacingxblock.core.SharedBlockBase
.xblock.core.XBlockAside
, each behaving the same as theirXBlock
counterpart:usage_key
context_key
index_dictionary
xblock.core.XBlockMixin
, encompassing the functionality of these former classes:xblock.mixins.IndexInfoMixin
xblock.mixins.XmlSerializationMixin
xblock.mixins.HandlersMixin
Bumps version from 2.0.0 to 3.0.0.
Testing
Docs
You can preview the doc additions by clicking "Details" on the docs build, hitting "View Docs", and going to the "XBlock API" page).
Homepage: https://docsopenedxorg--718.org.readthedocs.build/projects/xblock/en/718/index.html
New XBlockAside generated docs: https://docsopenedxorg--718.org.readthedocs.build/projects/xblock/en/718/xblock.html#xblock.core.XBlockAside
XBlock and XBlockAside attributes
From the root of the repo, you can run this script in order to compare the list of attributes on XBlock/XBlockAside before and after this PR, both for the classes and their instances.
This should yield:
which indicates that:
context_key
,usage_key
andindex_dictionary
attributes