Skip to content
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

Merged
merged 3 commits into from
Mar 19, 2024

Conversation

kdmccormick
Copy link
Member

@kdmccormick kdmccormick commented Feb 12, 2024

Supporting info

Proposed in this DEPR issue:

excerpt:

The implementations of the XBlock and XBlockAside classes are chopped into about a dozen tiny mixins. This was done many years ago, probably for some mix of reasons including code organization and theoretical reusability. Now, in 2024 in the openedx GitHub org, there are uses of the XBlock, XBlockAside, and XBlockMixin classes, but we do not see uses of any of the other classes or mixins (other than one extraneous reference to HandlersMixin in edx-platform, which can be removed).

Unfortunately, these mixins make it harder/impossible to write robust type constraints for XBlock and XBlockAside. They also make it harder to find code in the XBlock package. See discussion.

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 and dot.

Before

xblock-before

After

xblock-after

Change list

  • Removed:
    • xblock.XBlockMixin (still available as xblock.core.XBlockMixin)
    • xblock.core.SharedBlockBase (replaced with xblock.core.Blocklike)
    • xblock.internal.Nameable
    • xblock.internal.NamedAttributesMetaclass
    • xblock.django.request.HeadersDict
    • xblock.fields.XBlockMixin (still available as xblock.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
  • Added:
    • xblock.core.Blocklike, the new common ancestor of XBlock and XBlockAside, and XBlockMixin, replacing xblock.core.SharedBlockBase.
    • New attributes on xblock.core.XBlockAside, each behaving the same as their XBlock counterpart:
      • usage_key
      • context_key
      • index_dictionary
    • Various new attributes on xblock.core.XBlockMixin, encompassing the functionality of these former classes:
      • xblock.mixins.IndexInfoMixin
      • xblock.mixins.XmlSerializationMixin
      • xblock.mixins.HandlersMixin
  • Docs
    • Various docstrings have been improved, some of which are published in the docs.
    • XBlockAside will now be represented in the API docs, right below XBlock on the "XBlock API" page.
    • XBlockMixin has been removed from the docs. It was only ever documented under the "Fields API" page (which didn't make any sense), and it was barely even documented there. We considered adding it back to the "XBlock API" page, but as noted in the class's new docstring, we do not want to encourage any new use of XBlockMixin.

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.

rm -rf dir_before dir_after
mkdir dir_before dir_after

cd dir_before
git switch master
python -c 'from xblock import core; print(*sorted(dir(core.XBlock)), sep="\n")' > block_cls.list
python -c 'from xblock import core; print(*sorted(dir(core.XBlock(None, None, None))), sep="\n")' > block_obj.list
python -c 'from xblock import core; print(*sorted(dir(core.XBlockAside)), sep="\n")'  > aside_cls.list
python -c 'from xblock import core; print(*sorted(dir(core.XBlockAside(None, None, runtime=None))), sep="\n")' > aside_obj.list
cd ..

cd dir_after
git switch kdmccormick/mixins
python -c 'from xblock import core; print(*sorted(dir(core.XBlock)), sep="\n")' > block_cls.list
python -c 'from xblock import core; print(*sorted(dir(core.XBlock(None, None, None))), sep="\n")' > block_obj.list
python -c 'from xblock import core; print(*sorted(dir(core.XBlockAside)), sep="\n")'  > aside_cls.list
python -c 'from xblock import core; print(*sorted(dir(core.XBlockAside(None, None, runtime=None))), sep="\n")' > aside_obj.list
cd ..

diff dir_before list dir_after

This should yield:

diff dir_before/aside_cls.list dir_after/aside_cls.list
39a40
> context_key
49a51
> index_dictionary
62a65
> usage_key
diff dir_before/aside_obj.list dir_after/aside_obj.list
42a43
> context_key
52a54
> index_dictionary
67a70
> usage_key
diff dir_before/block_cls.list dir_after/block_cls.list
0a1
> __annotations__
diff dir_before/block_obj.list dir_after/block_obj.list
0a1
> __annotations__

which indicates that:

  • XBlockAsides now have context_key, usage_key and index_dictionary attributes
  • XBlocks have a few type annotations now (more to come in Add type hints and check them in CI #707 )
  • The list of attributes is otherwise unchanged

@kdmccormick kdmccormick force-pushed the kdmccormick/mixins branch 2 times, most recently from 9c06865 to 21ce2ab Compare February 12, 2024 19:00
Copy link
Member Author

@kdmccormick kdmccormick left a 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'),
Copy link
Member Author

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.

Comment on lines -15 to -28
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
Copy link
Member Author

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.

Comment on lines +29 to +34
# 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"),
])
Copy link
Member Author

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']
Copy link
Member Author

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):
Copy link
Member Author

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):
Copy link
Member Author

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.

Comment on lines +342 to +348
@staticmethod
def needs_name(field):
"""
Returns whether the given ) is yet to be named.
"""
return not field.__name__

Copy link
Member Author

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.

Suggested change
@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):
Copy link
Member Author

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.

Comment on lines 438 to 439
@Blocklike.needs("field-data")
class FieldTester(Blocklike):
Copy link
Member Author

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").

Copy link
Contributor

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)
Copy link
Member Author

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.

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
Copy link
Contributor

@ormsbee ormsbee left a 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
Comment on lines 633 to 635
(...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 :)
Copy link
Contributor

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.

Copy link
Member Author

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.

Copy link
Member Author

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):
Copy link
Contributor

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)?

Copy link
Member Author

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 ?

Copy link
Contributor

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.

Copy link
Member Author

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/core.py Show resolved Hide resolved
Comment on lines 438 to 439
@Blocklike.needs("field-data")
class FieldTester(Blocklike):
Copy link
Contributor

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")

@kdmccormick
Copy link
Member Author

@ormsbee

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?

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)

@kdmccormick kdmccormick merged commit f79bfc5 into openedx:master Mar 19, 2024
10 checks passed
@kdmccormick kdmccormick deleted the kdmccormick/mixins branch March 19, 2024 18:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants