Skip to content

Commit 116aeca

Browse files
lawremihadley
andauthored
Support S7 extension of S4 classes and vice-versa (#659)
* use isClassUnion() instead of direct class check * pass className attribute directly to preserve package * since proper S4 classes can extend old classes, be stricter when reducing to an S3 class, and use methods::extends() to capture the entire class vector, not just the first class * fix is_oldClass() and use it in S4_to_S7_class() * initial implementation passing a simple test that does not depend on @() behavior, which needs to change in base R. * route S4 class registration through S4_register() and setOldClass(); S4_register() can now communicate any S7 class structure through the S4class= argument. * add a .lintr.R to allow for our symbol naming convention * S4-derived classes gain an initialize() method that mimics the default initialize() except uses the S7 property setting path. * call S4_register() automatically on S7 derivatives of S4 classes; fix parent object construction in constructor. * add some detailed notes on S4 compatibility to docs * fix S7 inheritance checks after rebase * drop the S7_object case from S4_register * docs: recommend use of initialize() when working with S7 derivatives of S4 * clean up inheritance logic changes * just remove classes that exist instead of using try() * update the global variables for our use of @, which still confuses codetools * clean up constructor generation * fix test guarding against extension of S4 classes * allow abstract classes to inhert from virtual S4 classes * clean up S4 subclass setOldClass() registration; now supports more complex hierarchies * more tweaks to subclass registration * make S7_class_name robust to invocation at package build time * reformat * convert() falls back to methods::as() * correct S4 class name for 'to' * add test * convert_up() can convert an S4-derived S7 object to an S4 object * clean up and test restricted methods::as() fallback to convert() * S4_register() returns the name of the class it registered, which can be convenient for passing to methods package functions * rename S4_transient_prototype_class to S4_register_prototype_class and have it return the class name. * support class unions in S4_register() * make S4_class() return a methods-friendly class name and have it handle unions, particularly class_numeric * support S4 classes extending S7 classes * move away from do.call(setClass, ...); package is not needed on the prototype class * revert unnecessary direct attr() access * move a couple more S4 helpers from methods-register.R to S4.R * S7 classes inherit S4 class unions from S4 parents directly, which simplifies the S4 -> S7 -> S4 inheritance case. * avoid inherited slot conflicts in the S4->S7->S4 inheritance pattern * instead of forwarding S4 class unions into S7 classes, we enable S4_union_class() to find existing S4 unions. * update test snapshot after rebase * represent inheritance from S7_object at the S4 level * since @<- will dispatch to S7 setting even on S4 objects (derived from S7), we need to use R_do_slot_assign so that a NULL value does not delete the S4 slot. * inherits2() in prop.c handles S4 object case where class attribute is stored in the .S3Class slot. * tweaks to validObject hooks to support additional inheritance patterns * in obj_type(), isS4() takes precedence over has_S4_class(), since an S4 object is just that, even if it inherits from an S7 class and thus carries an S7_class slot. * restore method-register-S4.R; got lost in the rebase shuffle * prop storage now uses the S4 sentinel for NULL for low-level compatibility with S4; arguably a generally better approach since it avoids adding and deleting attributes based on whether the value is NULL or not. * push S4 validation guard down into validate() so that it works for validate() as well as validObject() * Define S4 prototype on the ::S4Slots old class based on property defaults. We evaluate these if they are language objects, so S7 classes extending or being extended by S4 classes need to ensure that evaluation can happen at build time. Also mark it VIRTUAL because it should never be constructed directly. * mark the subclass old class as virtual, because there should never be an S4 instance of that class (old classes are virtual normally but not in this case since we pass an S4Class prototype) * give convert_up() a general as()-based fallback like convert() * make the ::S4Slots class non-virtual to enable more convenient construction * drop the up cast coercions (dead end) and instead ensure full slot representation along the old class chain * S3 method registration will also register an S4 method when the generic is internal and an element of the signature has an S4 ancestor. This is needed because internal generics will favor S4 methods on S4 objects, so there is potential for inheriting overrides. * abstract S7 classes deriving from S4 classes are represented as ordinary S4 classes instead of old classes, because an old class implies an S3 instance of the object can exist, ie, there can be an S3Part(). * old classes were missing S7_class as a declared slot, which can cause it to get stripped during S4 upcasting, breaking the S7 object * shorten the S4 registration helper name * allow multi-argument signature on internal generics via S4 generic definition * S4_initialize sets slots that aren't properties using slot<-, not prop<- * @<- supports setting slots on S4 objects * S4_validate_shim() does not try to validate objects from a "cousin" S7 class * adapt validObject() call to S7 validate contract * eliminate S7_object::S4Slots from hierarchy to simplify inheritance; in principle, there should only be one ::S4Slots shim for each S4 derivative of an S7 class * doc updates * wording tweak Co-authored-by: Hadley Wickham <h.wickham@gmail.com> * Another wording tweak Co-authored-by: Hadley Wickham <h.wickham@gmail.com> * rename is_multi_arg_signature to is_plain_list to clarify its (more general) purpose * clarify S4_register() by moving early returns earlier * move S4 inheritance guidance to the compatbility vignette * massive simplification: no longer define ::S4Slots variant to enable S4 to inherit from S7; there's just two types of classes: the stub old class and the reified old class. The former for when only dispatch is needed, the latter for when inheritance (in either direction) is needed. * add NEWS entry * post-rebase cleanup * revert accidental reversion of switch to := syntax * tighten up a test * fix local_S4_class(where=); bug was exposed by our stricter version of S4_remove_classes(). * API rework: S4_register() always includes properties as slots (what contains=TRUE used to do), which ensures that classes are always able to be used as S4 superclasses. Introduce a separate helper S4_contains() that just ensures the S7 class is a suitable parent of an S4 class. * fix regression from introduction of external generics * update for rename of @S7_class to @_S7_class * fix a few style / rebase issues --------- Co-authored-by: Hadley Wickham <h.wickham@gmail.com>
1 parent 9eb5abb commit 116aeca

28 files changed

Lines changed: 1880 additions & 128 deletions

.Rbuildignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,4 @@
2323
^\.claude$
2424
^\.git-blame-ignore-revs$
2525
^AGENTS\.md$
26+
^\.lintr\.R$

.lintr.R

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
linters <- lintr::linters_with_defaults()

NAMESPACE

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ export("S7_data<-")
4242
export("method<-")
4343
export("prop<-")
4444
export("props<-")
45+
export(S4_contains)
4546
export(S4_register)
4647
export(S7_class)
4748
export(S7_class_desc)

NEWS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
* New `:=` operator creates and names an object in one step, so `Foo := new_class()` is equivalent to `Foo <- new_class(name = "Foo")` (#658).
44
* The class object that S7 stores on each instance now lives in the `_S7_class` attribute (previously `S7_class`), moving it into the `_`-prefixed namespace reserved for S7 internals so it can't collide with a user-defined property. Objects created by an older version of S7 (e.g. serialised to disk or baked into another package's lazy-load database) continue to work, as S7 falls back to the old attribute name when reading them (#677).
5+
* S7 and S4 now interoperate through inheritance. `new_class()` can use an S4 class as a parent, mapping S4 slots to S7 properties and registering the class with S4 automatically. Conversely, `S4_register()` registers an S7 class with S4, and `S4_contains()` returns an S4 class name suitable for `methods::setClass(contains = )`, exposing stored S7 properties as S4 slots for S4 subclasses. This support includes S4 initialization and validity integration, and S4/internal generic registration where needed; see `vignette("compatibility")` for caveats (#456).
56
* Errors thrown by S7 now report the function where they occurred, making it easier to track down the source of a problem (#646).
67
* `class_POSIXct` uses the `tzone` attribute (not `tz`), and allows it to be absent (#401).
78
* Base type wrappers like `class_integer` now define their constructor and validator in the S7 namespace. (#553).

0 commit comments

Comments
 (0)