python-interactive-courses 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. pyics/__init__.py +0 -0
  2. pyics/app/actions.py +179 -0
  3. pyics/app/cli.py +335 -0
  4. pyics/app/key_listener.py +133 -0
  5. pyics/app/prompts.py +439 -0
  6. pyics/app/pyics.py +454 -0
  7. pyics/data/__init__.py +4 -0
  8. pyics/data/course.py +531 -0
  9. pyics/data/task.py +744 -0
  10. pyics/distribution/__init__.py +5 -0
  11. pyics/distribution/archive.py +667 -0
  12. pyics/distribution/dependencies.py +452 -0
  13. pyics/distribution/fetch.py +128 -0
  14. pyics/distribution/pull.py +376 -0
  15. pyics/distribution/slugs.py +24 -0
  16. pyics/distribution/sources.py +561 -0
  17. pyics/notebook/__init__.py +1 -0
  18. pyics/notebook/cells.py +184 -0
  19. pyics/notebook/loader.py +590 -0
  20. pyics/notebook/sections.py +189 -0
  21. pyics/plugins.py +90 -0
  22. pyics/scripts/console.py +86 -0
  23. pyics/scripts/create_course.py +301 -0
  24. pyics/scripts/create_task.py +850 -0
  25. pyics/scripts/edit_course.py +133 -0
  26. pyics/scripts/edit_task.py +109 -0
  27. pyics/scripts/main.py +118 -0
  28. pyics/scripts/pack.py +198 -0
  29. pyics/scripts/pull.py +123 -0
  30. pyics/scripts/sources.py +118 -0
  31. pyics/tests/__init__.py +3 -0
  32. pyics/tests/capture.py +114 -0
  33. pyics/tests/check_specs.py +528 -0
  34. pyics/tests/check_structure.py +769 -0
  35. pyics/tests/compare.py +184 -0
  36. pyics/tests/pytest_integration.py +228 -0
  37. pyics/tests/results.py +145 -0
  38. pyics/tests/tests.py +440 -0
  39. pyics/tests/trace.py +156 -0
  40. pyics/util/discovery.py +92 -0
  41. pyics/util/json_handling.py +182 -0
  42. pyics/util/logging.py +39 -0
  43. pyics/util/paths.py +38 -0
  44. pyics/util/terminal.py +57 -0
  45. python_interactive_courses-1.0.0.dist-info/METADATA +245 -0
  46. python_interactive_courses-1.0.0.dist-info/RECORD +49 -0
  47. python_interactive_courses-1.0.0.dist-info/WHEEL +4 -0
  48. python_interactive_courses-1.0.0.dist-info/entry_points.txt +3 -0
  49. python_interactive_courses-1.0.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,528 @@
1
+ """
2
+ Compare symbols against a specification.
3
+
4
+ Specifications are plain dicts, so that they can be stored as json. Classes and callables
5
+ are accepted as well, and converted using ``to_spec_dict``.
6
+
7
+ Spec dict format
8
+ ----------------
9
+ A function spec maps parameter names to parameter specs, plus a ``return`` type::
10
+
11
+ {"is_class": False, "x": "int", "flag": None, "return": "bool"}
12
+
13
+ A parameter spec is either a type string, ``None`` (no type hint required), or a dict with
14
+ the (optional) keys ``type``, ``default`` and ``kind``::
15
+
16
+ {"type": "int", "default": 0, "kind": "KEYWORD_ONLY"}
17
+
18
+ A missing ``kind`` means ``POSITIONAL_OR_KEYWORD``, a missing ``default`` means that no
19
+ default value is expected. ``self``/``cls`` are never listed.
20
+
21
+ A class spec maps attribute names to type strings (or ``None``), and method, property and
22
+ nested class names to nested specs::
23
+
24
+ {
25
+ "is_class": True,
26
+ "name": "str",
27
+ "__init__": {"is_class": False, "name": "str", "return": "None"},
28
+ "size": {
29
+ "is_class": False,
30
+ "is_property": True,
31
+ "return": "int",
32
+ "setter": False,
33
+ "deleter": False,
34
+ },
35
+ }
36
+
37
+ Class specs include inherited attributes, methods, properties and nested classes. A class
38
+ without an ``__init__`` is compared as if it had ``def __init__(self) -> None``.
39
+
40
+ ``is_class`` and ``return`` are reserved keys, so they can not be used as parameter or
41
+ attribute names.
42
+ """
43
+
44
+ import annotationlib
45
+ import builtins
46
+ import inspect
47
+ import re
48
+ import sys
49
+ import typing
50
+ from collections.abc import Callable
51
+ from dataclasses import dataclass, field
52
+ from typing import Any
53
+
54
+ from rich.markup import escape
55
+
56
+ SpecDict = dict[str, Any]
57
+ SpecType = Callable | type | SpecDict
58
+
59
+ _POK = inspect.Parameter.POSITIONAL_OR_KEYWORD
60
+ _KINDS = {kind.name: kind for kind in type(_POK)}
61
+ _PARAM_KEYS = {"type", "default", "kind"}
62
+ _PROPERTY_KEYS = {"is_class", "is_property", "return", "setter", "deleter"}
63
+ # dunder methods which are part of a class spec, all others are ignored
64
+ _CHECKED_DUNDERS = ("__init__", "__call__", "__str__", "__repr__")
65
+ # module prefixes stripped from type strings, in addition to the symbol's own module
66
+ _STRIPPED_MODULES = ("typing", "collections.abc", "builtins")
67
+ _NO_DEFAULT = object()
68
+ # compared against if a class defines no ``__init__``, i.e. inherits ``object.__init__``
69
+ _IMPLICIT_INIT: SpecDict = {"is_class": False, "return": "None"}
70
+
71
+
72
+ def compare_spec(
73
+ expected: SpecType,
74
+ other: Any,
75
+ *,
76
+ by_type: bool = False,
77
+ use_rich_markup: bool = True,
78
+ flag_type_hints: bool = True,
79
+ exact: bool = False,
80
+ ) -> list[str]:
81
+ """
82
+ Compare ``other`` against an ``expected`` specification.
83
+
84
+ Parameters
85
+ ----------
86
+ expected: SpecType
87
+ The expected specification: a spec dict (see module docstring), a class, or a
88
+ function
89
+ other: Any
90
+ The symbol to compare against ``expected``
91
+ by_type: bool (optional, keyword only)
92
+ Default False. If set, match parameters and attributes purely by their type,
93
+ ignoring their names and order. Methods, properties and nested classes are still
94
+ matched by name, as they define the class' public API.
95
+ use_rich_markup: bool (optional, keyword only)
96
+ Use ``rich`` markup in diff strings for additional highlighting.
97
+ Default True.
98
+ flag_type_hints: bool (optional, keyword only)
99
+ Default True. If set to false, do not flag missing or mismatching type hints.
100
+ Ignored if ``by_type`` is set.
101
+ exact: bool (optional, keyword only)
102
+ Require an exact, i.e. two-way match (no extra parameters/attributes/methods on
103
+ the other side, no default values on the other side if none are expected)
104
+
105
+ Returns
106
+ -------
107
+ list[str]
108
+ List of differences between ``expected`` and ``other``. Empty if match.
109
+
110
+ Raises
111
+ ------
112
+ TypeError
113
+ if ``expected`` is of an invalid type
114
+ ValueError
115
+ if ``expected`` is a malformed spec dict
116
+ """
117
+ exp = to_spec_dict(expected)
118
+ module = getattr(other, "__module__", None)
119
+ opts = _Options(
120
+ by_type=by_type,
121
+ rich=use_rich_markup,
122
+ check_types=by_type or flag_type_hints,
123
+ exact=exact,
124
+ module=module,
125
+ namespace=vars(sys.modules[module]) if module in sys.modules else {},
126
+ )
127
+
128
+ if exp["is_class"]:
129
+ if not inspect.isclass(other):
130
+ return [f"Expected {opts.b('class')}, got {opts.b(type(other))}"]
131
+ elif not (inspect.isfunction(other) or inspect.ismethod(other)):
132
+ return [f"Expected a {opts.b('function/callable')}, got {opts.b(type(other))}"]
133
+
134
+ try:
135
+ oth = to_spec_dict(other)
136
+ except ValueError:
137
+ return ["No valid callable found."]
138
+
139
+ return _compare(exp, oth, opts)
140
+
141
+
142
+ def to_spec_dict(obj: SpecType) -> SpecDict:
143
+ """
144
+ Convert a class or function into a spec dict (see module docstring). Spec dicts are
145
+ validated and returned as-is.
146
+
147
+ Raises
148
+ ------
149
+ TypeError
150
+ if ``obj`` is neither a class, a function, nor a dict
151
+ ValueError
152
+ if ``obj`` is a malformed spec dict
153
+ """
154
+ if isinstance(obj, dict):
155
+ _validate(obj, "spec")
156
+ return obj
157
+ if inspect.isclass(obj):
158
+ return _class_to_dict(obj)
159
+ if inspect.isfunction(obj) or inspect.ismethod(obj):
160
+ return _func_to_dict(obj, obj.__module__)
161
+ raise TypeError("Expected specification must be a class, function, or spec dict.")
162
+
163
+
164
+ # conversion ###########################################################################
165
+
166
+
167
+ def _format_type(annotation: Any, module: str | None) -> str | None:
168
+ """format a type annotation as a string, without module prefixes"""
169
+ if annotation is inspect.Parameter.empty:
170
+ return None
171
+ if isinstance(annotation, annotationlib.ForwardRef):
172
+ annotation = annotation.__forward_arg__
173
+ if isinstance(annotation, str):
174
+ return " ".join(annotation.split())
175
+
176
+ text = inspect.formatannotation(annotation)
177
+ for mod in (*_STRIPPED_MODULES, module):
178
+ if mod:
179
+ text = re.sub(rf"(?<![\w.]){re.escape(mod)}\.", "", text)
180
+ return text
181
+
182
+
183
+ def _func_to_dict(func: Callable, module: str | None, *, drop_first=False) -> SpecDict:
184
+ sig = inspect.signature(func, annotation_format=annotationlib.Format.FORWARDREF)
185
+ params = list(sig.parameters.values())[1 if drop_first else 0 :]
186
+
187
+ spec: SpecDict = {"is_class": False}
188
+ for p in params:
189
+ type_str = _format_type(p.annotation, module)
190
+ if p.kind is _POK and p.default is p.empty:
191
+ spec[p.name] = type_str
192
+ continue
193
+
194
+ param: dict[str, Any] = {"type": type_str}
195
+ if p.kind is not _POK:
196
+ param["kind"] = p.kind.name
197
+ if p.default is not p.empty:
198
+ param["default"] = p.default
199
+ spec[p.name] = param
200
+
201
+ spec["return"] = _format_type(sig.return_annotation, module)
202
+ return spec
203
+
204
+
205
+ def _property_to_dict(prop: property, module: str | None) -> SpecDict:
206
+ getter = _func_to_dict(prop.fget, module, drop_first=True) if prop.fget else {}
207
+ return {
208
+ "is_class": False,
209
+ "is_property": True,
210
+ "return": getter.get("return"),
211
+ "setter": prop.fset is not None,
212
+ "deleter": prop.fdel is not None,
213
+ }
214
+
215
+
216
+ def _class_to_dict(cls: type) -> SpecDict:
217
+ spec: SpecDict = {"is_class": True}
218
+ # the class itself and its bases, most derived first. ``object`` contributes no
219
+ # Python functions, its dunders would only be noise
220
+ bases = [klass for klass in cls.__mro__ if klass is not object]
221
+
222
+ # attributes: annotated ones and those assigned in methods, incl. inherited ones
223
+ hints = typing.get_type_hints(cls, format=annotationlib.Format.FORWARDREF)
224
+ static = [a for k in reversed(bases) for a in getattr(k, "__static_attributes__", ())]
225
+ for name in dict.fromkeys([*hints, *static]):
226
+ spec[name] = _format_type(hints.get(name, inspect.Parameter.empty), cls.__module__)
227
+
228
+ # methods, properties and nested classes, incl. inherited ones. The most derived
229
+ # definition of a name wins, even if it is no method (e.g. ``method = None``)
230
+ seen: set[str] = set()
231
+ for klass in bases:
232
+ module = klass.__module__
233
+ for name, member in klass.__dict__.items():
234
+ if name in seen or (name.startswith("__") and name not in _CHECKED_DUNDERS):
235
+ continue
236
+ seen.add(name)
237
+
238
+ if isinstance(member, staticmethod):
239
+ spec[name] = _func_to_dict(member.__func__, module)
240
+ elif isinstance(member, classmethod):
241
+ spec[name] = _func_to_dict(member.__func__, module, drop_first=True)
242
+ elif inspect.isfunction(member):
243
+ spec[name] = _func_to_dict(member, module, drop_first=True)
244
+ elif isinstance(member, property):
245
+ spec[name] = _property_to_dict(member, module)
246
+ elif (
247
+ inspect.isclass(member)
248
+ and member.__qualname__ == f"{klass.__qualname__}.{name}"
249
+ ):
250
+ spec[name] = _class_to_dict(member)
251
+
252
+ return spec
253
+
254
+
255
+ # validation ###########################################################################
256
+
257
+
258
+ def _param(value: Any) -> dict[str, Any]:
259
+ """normalize a parameter spec to a dict with a ``type``, ``kind`` and optionally a
260
+ ``default`` key"""
261
+ if not isinstance(value, dict):
262
+ return {"type": value, "kind": _POK.name}
263
+ param = {"type": value.get("type"), "kind": value.get("kind", _POK.name).upper()}
264
+ if "default" in value:
265
+ param["default"] = value["default"]
266
+ return param
267
+
268
+
269
+ def _validate(spec: SpecDict, path: str) -> None:
270
+ if not isinstance(spec.get("is_class"), bool):
271
+ raise ValueError(f"{path}: must have a boolean 'is_class' key.") # noqa: TRY004
272
+
273
+ if spec.get("is_property"):
274
+ if unknown := spec.keys() - _PROPERTY_KEYS:
275
+ raise ValueError(f"{path}: unknown property keys {sorted(unknown)}.")
276
+ return
277
+
278
+ for name, value in spec.items():
279
+ if name in ("is_class", "return"):
280
+ if name == "return" and spec["is_class"]:
281
+ raise ValueError(f"{path}: 'return' is reserved for function specs.")
282
+ continue
283
+
284
+ sub_path = f"{path}.{name}"
285
+ if spec["is_class"] and isinstance(value, dict):
286
+ _validate(value, sub_path)
287
+ elif isinstance(value, dict):
288
+ if unknown := value.keys() - _PARAM_KEYS:
289
+ raise ValueError(f"{sub_path}: unknown parameter keys {sorted(unknown)}.")
290
+ kind = value.get("kind", _POK.name)
291
+ if not isinstance(kind, str) or kind.upper() not in _KINDS:
292
+ raise ValueError(f"{sub_path}: kind must be one of {list(_KINDS)}.")
293
+ if not isinstance(value.get("type"), str | None):
294
+ raise ValueError(f"{sub_path}: type must be a string or None.")
295
+ elif not isinstance(value, str | None):
296
+ raise ValueError(f"{sub_path}: must be a type string, None, or a dict.")
297
+
298
+
299
+ # comparison ###########################################################################
300
+
301
+
302
+ @dataclass
303
+ class _Options:
304
+ by_type: bool
305
+ rich: bool
306
+ check_types: bool
307
+ exact: bool
308
+ module: str | None
309
+ namespace: dict[str, Any]
310
+ _types: dict[str, str] = field(default_factory=dict)
311
+
312
+ def b(self, text: Any) -> str:
313
+ """bold ``text``, if rich markup is used"""
314
+ return f"[b]{escape(str(text))}[/]" if self.rich else str(text)
315
+
316
+ def q(self, name: str) -> str:
317
+ """quote and bold ``name``"""
318
+ return self.b(f"'{name}'")
319
+
320
+ def type(self, type_str: str | None) -> str | None:
321
+ """normalize a type string by evaluating it in the compared symbol's module, so
322
+ e.g. ``Optional[int]`` and ``int | None`` compare equal"""
323
+ if type_str is None:
324
+ return None
325
+ if type_str not in self._types:
326
+ try:
327
+ globals_ = {"__builtins__": builtins, **vars(typing), **self.namespace}
328
+ normalized = _format_type(eval(type_str, globals_), self.module)
329
+ except Exception: # noqa: BLE001 - not a valid expression, compare as-is
330
+ normalized = type_str
331
+ self._types[type_str] = normalized or type_str
332
+ return self._types[type_str]
333
+
334
+
335
+ def _describe(spec: SpecDict) -> str:
336
+ if spec.get("is_property"):
337
+ return "property"
338
+ return "class" if spec["is_class"] else "method"
339
+
340
+
341
+ def _compare(exp: SpecDict, oth: SpecDict, o: _Options) -> list[str]:
342
+ if exp.get("is_property"):
343
+ return _compare_property(exp, oth, o)
344
+ if exp["is_class"]:
345
+ return _compare_class(exp, oth, o)
346
+ return _compare_func(exp, oth, o)
347
+
348
+
349
+ def _compare_type(label: str, exp_t: str | None, oth_t: str | None, o: _Options):
350
+ if not o.check_types or exp_t is None:
351
+ return []
352
+ exp_t, oth_t = o.type(exp_t), o.type(oth_t)
353
+ if oth_t is None:
354
+ return [f"{label} is missing its type hint."]
355
+ if exp_t != oth_t:
356
+ return [f"{label} type mismatch. Expected {o.b(exp_t)}, got {o.b(oth_t)}."]
357
+ return []
358
+
359
+
360
+ def _match_by_type(
361
+ what: str, exp_types: list[str | None], oth_types: list[str | None], o: _Options
362
+ ) -> list[str]:
363
+ """match two lists of types as multisets. ``None`` in ``exp_types`` matches any type,
364
+ after all typed entries have been matched"""
365
+ diffs = []
366
+ unmatched = [o.type(t) for t in oth_types]
367
+
368
+ for t in (o.type(t) for t in exp_types if t is not None):
369
+ if t in unmatched:
370
+ unmatched.remove(t)
371
+ else:
372
+ diffs.append(f"Missing {what} of type {o.b(t)}.")
373
+
374
+ for _ in (t for t in exp_types if t is None):
375
+ if unmatched:
376
+ unmatched.pop(0)
377
+ else:
378
+ diffs.append(f"Missing {what}.")
379
+
380
+ if o.exact:
381
+ diffs.extend(
382
+ f"Additional {what} of type {o.b(t)}." if t else f"Additional {what}."
383
+ for t in unmatched
384
+ )
385
+ return diffs
386
+
387
+
388
+ def _compare_func(exp: SpecDict, oth: SpecDict, o: _Options) -> list[str]:
389
+ exp_params = {n: _param(v) for n, v in exp.items() if n not in ("is_class", "return")}
390
+ oth_params = {n: _param(v) for n, v in oth.items() if n not in ("is_class", "return")}
391
+
392
+ if o.by_type:
393
+ diffs = _match_by_type(
394
+ "parameter",
395
+ [p["type"] for p in exp_params.values()],
396
+ [p["type"] for p in oth_params.values()],
397
+ o,
398
+ )
399
+ else:
400
+ diffs = _compare_params_by_name(exp_params, oth_params, o)
401
+
402
+ diffs.extend(_compare_type("Return value", exp.get("return"), oth.get("return"), o))
403
+ return diffs
404
+
405
+
406
+ def _compare_params_by_name(
407
+ exp_params: dict[str, dict], oth_params: dict[str, dict], o: _Options
408
+ ) -> list[str]:
409
+ diffs = []
410
+ for name, exp_p in exp_params.items():
411
+ if name not in oth_params:
412
+ diffs.append(f"Missing parameter: {o.q(name)}.")
413
+ continue
414
+ oth_p = oth_params[name]
415
+
416
+ diffs.extend(
417
+ _compare_type(f"Parameter {o.q(name)}", exp_p["type"], oth_p["type"], o)
418
+ )
419
+
420
+ exp_default = exp_p.get("default", _NO_DEFAULT)
421
+ oth_default = oth_p.get("default", _NO_DEFAULT)
422
+ if exp_default is _NO_DEFAULT and oth_default is not _NO_DEFAULT and o.exact:
423
+ diffs.append(f"Parameter {o.q(name)} should not have a default value.")
424
+ elif exp_default is not _NO_DEFAULT and oth_default is _NO_DEFAULT:
425
+ diffs.append(
426
+ f"Parameter {o.q(name)} is missing its default value "
427
+ f"{o.b(repr(exp_default))}."
428
+ )
429
+ elif exp_default is not _NO_DEFAULT and exp_default != oth_default:
430
+ diffs.append(
431
+ f"Parameter {o.q(name)} default value mismatch. Expected "
432
+ f"{o.b(repr(exp_default))}, got {o.b(repr(oth_default))}."
433
+ )
434
+
435
+ if exp_p["kind"] != oth_p["kind"]:
436
+ diffs.append(
437
+ f"Parameter {o.q(name)} kind mismatch. Expected "
438
+ f"{o.b(_KINDS[exp_p['kind']].description)}, got "
439
+ f"{o.b(_KINDS[oth_p['kind']].description)}."
440
+ )
441
+
442
+ if o.exact:
443
+ diffs.extend(
444
+ f"Additional parameter: {o.q(name)}."
445
+ for name in oth_params
446
+ if name not in exp_params
447
+ )
448
+ return diffs
449
+
450
+
451
+ def _compare_property(exp: SpecDict, oth: SpecDict, o: _Options) -> list[str]:
452
+ diffs = _compare_type("Getter return value", exp.get("return"), oth.get("return"), o)
453
+ for key in ("setter", "deleter"):
454
+ if exp.get(key) and not oth.get(key):
455
+ diffs.append(f"Missing {key}.")
456
+ elif oth.get(key) and not exp.get(key):
457
+ diffs.append(f"Should not have a {key}.")
458
+ return diffs
459
+
460
+
461
+ def _compare_class(exp: SpecDict, oth: SpecDict, o: _Options) -> list[str]:
462
+ def split(spec: SpecDict) -> tuple[dict, dict]:
463
+ items = [(k, v) for k, v in spec.items() if k != "is_class"]
464
+ return (
465
+ {k: v for k, v in items if not isinstance(v, dict)},
466
+ {k: v for k, v in items if isinstance(v, dict)},
467
+ )
468
+
469
+ exp_attrs, exp_members = split(exp)
470
+ oth_attrs, oth_members = split(oth)
471
+ diffs = []
472
+
473
+ # attributes
474
+ if o.by_type:
475
+ diffs.extend(
476
+ _match_by_type(
477
+ "attribute", list(exp_attrs.values()), list(oth_attrs.values()), o
478
+ )
479
+ )
480
+ else:
481
+ for name, exp_t in exp_attrs.items():
482
+ if name in oth_members:
483
+ diffs.append(
484
+ f"{o.q(name)} should be an {o.b('attribute')}, but found a "
485
+ f"{o.b(_describe(oth_members[name]))}."
486
+ )
487
+ elif name not in oth_attrs:
488
+ diffs.append(f"Missing attribute: {o.q(name)}.")
489
+ else:
490
+ diffs.extend(
491
+ _compare_type(f"Attribute {o.q(name)}", exp_t, oth_attrs[name], o)
492
+ )
493
+ if o.exact:
494
+ diffs.extend(
495
+ f"Additional attribute: {o.q(name)}."
496
+ for name in oth_attrs
497
+ if name not in exp_attrs and name not in exp_members
498
+ )
499
+
500
+ # methods, properties and nested classes, always matched by name
501
+ for name, exp_m in exp_members.items():
502
+ what = _describe(exp_m)
503
+ if name == "__init__" and name not in oth_members and name not in oth_attrs:
504
+ oth_members = {**oth_members, name: _IMPLICIT_INIT}
505
+ if name in oth_members and _describe(oth_members[name]) == what:
506
+ diffs.extend(
507
+ f"{what.capitalize()} {o.q(name)}: {d}"
508
+ for d in _compare(exp_m, oth_members[name], o)
509
+ )
510
+ elif name in oth_members:
511
+ diffs.append(
512
+ f"{o.q(name)} should be a {o.b(what)}, but found a "
513
+ f"{o.b(_describe(oth_members[name]))}."
514
+ )
515
+ elif name in oth_attrs and not o.by_type:
516
+ diffs.append(
517
+ f"{o.q(name)} should be a {o.b(what)}, but found an {o.b('attribute')}."
518
+ )
519
+ else:
520
+ diffs.append(f"Missing {what}: {o.q(name)}.")
521
+
522
+ if o.exact:
523
+ diffs.extend(
524
+ f"Additional {_describe(oth_m)}: {o.q(name)}."
525
+ for name, oth_m in oth_members.items()
526
+ if name not in exp_members and (o.by_type or name not in exp_attrs)
527
+ )
528
+ return diffs