#!/usr/bin/env python3 """One interpreter start for the whole of 99-python3-fields. Emits `key=value' lines, one per line, in file order; the suite reads them with grep. A python3 start on this arm is tens of seconds and every measurement here builds a small class or reads an attribute -- microseconds beside the start -- so taking the start ONCE is the difference between a half-minute suite and a twenty-minute one. THE TRANSPORT RULES. The runner parses its input line by line, so no value may contain a NEWLINE; the suite splits on the FIRST `=', so a value containing `=' is safe and a key containing one is not. Nothing emitted below carries a memory address: every value is an int, a str, a bool or a comma-joined list of those. The GENERATED CLASSES ARE NEVER REPR'D as classes -- `_Factory' builds `Fields.a.b' style names that are stable, but a sealed class's `repr' is `' with a module prefix that differs between a probe run and an import, so only `__name__' is ever emitted. WHAT THIS PACKAGE IS. A metaclass (`_Factory') that turns attribute access into a field declaration -- `Fields.a.b.c' -- and a set of `sealers' that turn a declaration into a real class with a generated `__init__', comparison methods and `__repr__'. Subclassing a factory is what SEALS it: `class P(Fields.a.b)' runs the sealer and P's base is the generated class. So every case below asserts something about a class this probe BUILT AT RUNTIME from a declaration, not about a module-level object. That matters on this arm for one reason worth naming up front: the generated `__init__' is produced by `compile()' on source text this library assembles as a string, then `exec'd. THIS PACKAGE RUNS THE INTERPRETER'S OWN COMPILER AT IMPORT-ADJACENT TIME, which is not something most pure-Python ports on this image do. `make_init_func' and the `init-*' cases are what say that path works here. THE CLASS NAMES ARE PART OF THE CONTRACT and are asserted, because they are how a traceback from a generated `__init__' is read. `make_init_func' names the function `__fields_init_for__a__b__' and the sealer names the base `FieldsBase_for__a__b'; the factory itself carries `Fields.a.b'. A release that changed any of them would keep every behavioural case green and make every traceback from this library unrecognisable. WHERE THE EXPECTED VALUES COME FROM. Every value the suite compares against was MEASURED by running THIS FILE on an x86-64 control at the same version -- fields 5.0.0 from the same PyPI sdist (sha256 31d4aa03d8d44e35df13c431de35136997f047a924a597d84f7bc209e1be5727, the recipe's own), under CPython 3.12 -- and then re-run there against THIS BUILD's own .deb payload rather than the sdist. This package declares no ext_modules, reads no sizeof, packs no struct and branches on nothing but the Python major version, so a difference between the two arms is a DEFECT and not a property. ONE VALUE IS EXPECTED TO DIFFER FROM THE CONTROL, and it is the one that says the transcript came from the wasm32 interpreter rather than from a host copy of the package: * `py_maxsize_bits' -- 32 here, 64 on the control. There is no second such value here: this package starts no thread, opens no socket and touches no device, so the thread gap (brintos/linux-in-the-browser#85) that `99-python3-cached-property' has to work around does not arise at all. THIS PROBE EXITS 0 EVEN WHEN THE IMPORT FAILS. fields is pure Python, so an import failure would be a platform fact about the interpreter rather than about this package, and turning it into a hundred empty-string mismatches would bury that fact rather than report it. The failure is reported as `import_ok=no' with the exception, the suite reds exactly one case for it, and every behavioural case is SKIPPED with that reason attached. """ import sys emitted = [] def emit(key, value): emitted.append(key) print('%s=%s' % (key, value)) def err(fn, *args, **kw): """The exception TYPE NAME a call raises, or `none' when it returns. The name only: a message carries quoting and, for some builtins, the repr of an argument, neither of which is worth pinning across two arms. Where the MESSAGE is the finding -- this library raises several distinct `TypeError's from `_Factory.__getattr__' and only the text tells them apart -- `errmsg' below is used instead. """ try: fn(*args, **kw) except BaseException as exc: return type(exc).__name__ return 'none' def errmsg(fn, *args, **kw): """`: ', or `none'. Newlines are flattened and the result truncated: the transport is one line per value. These messages are the package's own literals, not the interpreter's, so they are arm-independent and worth pinning in full. """ try: fn(*args, **kw) except BaseException as exc: return '%s: %s' % (type(exc).__name__, str(exc).replace('\n', ' ')[:200]) return 'none' # ------------------------------------------------------------------ import --- # Pure Python, so this is expected to be uneventful -- but it is the first thing # that runs on this arm and every case below depends on it. # # NOTE that `fields/__init__.py' imports `linecache', `zlib' and `itertools' at # top level, UNGUARDED, while the .deb declares `Depends: python3-core' and # nothing else. `zlib' in particular is a SEPARATE sub-package of the ported # python3 (BRINTOS-1476) -- see the `dep-*' cases in the suite, which assert # that pair. try: import fields emit('import_ok', 'yes') emit('import_error', '') except BaseException as exc: emit('import_ok', 'no') emit('import_error', '%s: %s' % (type(exc).__name__, str(exc).replace('\n', ' ')[:200])) emit('emitted_count', str(len(emitted) + 1)) emit('probe_done', 'yes') sys.exit(0) # ---------------------------------------------------------------- identity --- # WHICH COPY ANSWERED. Without this, every case below would be a statement # about whatever was first on sys.path. `module_file' differs between the # control and the target by construction, so the suite asserts it directly # against the target's own path rather than comparing it to a control value. emit('module_file', getattr(fields, '__file__', '')) # IT IS A PACKAGE, NOT A MODULE. setup.py uses `find_packages("src")' with # `package_dir={"": "src"}', so the payload is a DIRECTORY -- which is why the # suite's payload cases stat a directory where a single-module port's stat a # file. `py_modules' is computed from `glob("src/*.py")', which matches nothing # in this sdist, so it contributes no second payload. emit('is_package', 'yes' if hasattr(fields, '__path__') else 'no') emit('version_attr', getattr(fields, '__version__', 'no-attr')) try: from importlib.metadata import version as _dv _dist = _dv('fields') except BaseException as exc: _dist = 'ERR:%s' % type(exc).__name__ emit('dist_version', _dist) emit('version_agrees', 'yes' if _dist == getattr(fields, '__version__', None) else 'no') # THERE IS AN `__all__', AND IT IS A TUPLE rather than the list most packages # ship. Pinned as both, because `from fields import *' is the documented entry # and a release that reordered or retyped it changes what a star-import binds. emit('has_all', 'yes' if hasattr(fields, '__all__') else 'no') emit('all_type', type(fields.__all__).__name__) emit('all_names', ','.join(fields.__all__)) # THE STAR-IMPORT SURFACE IS NARROWER THAN THE MODULE'S PUBLIC NAMES, and the # gap is the finding. `InheritableFields' -- the ONLY factory that supports the # multiple-inheritance form upstream's own `test_multiple_inheritance' uses -- # is a public module attribute and is NOT in `__all__', so `from fields import *' # does not bind it. `PY2' and `MISSING' are the same shape. Both sets are # pinned so the omission is a stated fact rather than something a reader has to # diff for. emit('public_names', ','.join(sorted(n for n in vars(fields) if not n.startswith('_')))) emit('all_missing_public', ','.join(sorted(set(n for n in vars(fields) if not n.startswith('_')) - set(fields.__all__)))) # PY2 IS FALSE, and the branch it guards is the one that would `exec' a Python 2 # statement. `make_init_func' ends in `if PY2: exec("exec codeobj in ...")', # which is a SyntaxError under 3.x -- it survives only because it sits inside a # string literal. This is the value that says which half of that branch every # generated `__init__' below came from. emit('py2_flag', 'yes' if fields.PY2 else 'no') # THE PY2 ORDEREDDICT BACKPORT SHIPS AND IS NEVER IMPORTED. `__init__.py' does # `try: from collections import OrderedDict / except ImportError: from # .py2ordereddict import OrderedDict', and `collections.OrderedDict' has existed # throughout 3.x -- so the fallback module is dead payload on this image. # Asserted as a pair: the module is IN the payload (the suite's `pkg-*' cases) # and it is NOT in `sys.modules' after an import of `fields'. emit('ordereddict_from_collections', 'yes' if fields.OrderedDict.__module__ == 'collections' else 'no') emit('py2ordereddict_imported', 'yes' if 'fields.py2ordereddict' in sys.modules else 'no') # `extras' IS A SEPARATE SUBMODULE and is NOT imported by the package's # `__init__'. So `import fields' does not bring `re' in with it, and # `fields.RegexValidate' does not exist until someone imports `fields.extras'. # Both halves are pinned -- the absence before, and the import working after. emit('extras_autoimported', 'yes' if 'fields.extras' in sys.modules else 'no') # ------------------------------------------------------- the factory itself --- # `Fields' IS A CLASS whose metaclass is `_Factory', not an instance of one. # That is what makes `Fields.a.b.c' work: each getattr returns a NEW class, and # subclassing the last one seals it. emit('fields_is_class', 'yes' if isinstance(fields.Fields, type) else 'no') emit('fields_metaclass', type(fields.Fields).__name__) # THE FACTORY'S OWN NAME CARRIES THE SEALER AND THE CHAIN, and it is the name a # `TypeError' from an unsealed factory prints. An empty factory names only the # sealer; a chained one appends the fields in declaration order. emit('bare_factory_name', fields.Fields.__name__) emit('chain_factory_name', fields.Fields.a.b.c.__name__) # EACH LINK IS A DISTINCT CLASS, rebuilt on every getattr -- the chain is not # memoised and `Fields.a' twice is two objects. This is what lets two # declarations that share a prefix not interfere. emit('chain_links_distinct', 'yes' if fields.Fields.a is not fields.Fields.a else 'no') # AN EMPTY FACTORY REFUSES TO SEAL. `~cls' is where sealing happens and it # raises rather than producing a fieldless class. The message is the package's # own literal. emit('empty_factory_error', errmsg(lambda: fields.Fields())) # DECLARING THE SAME FIELD TWICE IS REFUSED, and the two messages differ by # which half of the state the name was already in. Both come from # `_Factory.__getattr__' and only the TEXT tells them apart -- `err' would # flatten both to `TypeError'. emit('dup_required_error', errmsg(lambda: fields.Fields.a.a)) emit('dup_default_error', errmsg(lambda: fields.Fields.a[1].a)) # A REQUIRED FIELD AFTER A DEFAULTED ONE IS *NOT* REFUSED AT DECLARATION TIME, # which the guard in `__getattr__' reads as though it were. That guard is # `if cls.__defaults and cls.__last_field is not None', and `__getitem__' builds # its successor WITHOUT a `last_field' -- so immediately after `Fields.a[1]' the # second half is false and `.b' is accepted in silence. # # The refusal lands at SEAL time instead, in `__invert__', where the same # condition is finally true. So the declaration that is wrong and the point at # which the user is told are two different lines of their program, and the # message names neither field. Pinned as a PAIR for that reason: the accepted # declaration and the deferred error. emit('required_after_default_declares', err(lambda: fields.Fields.a[1].b)) emit('required_after_default_seal_error', errmsg(lambda: ~(fields.Fields.a[1].b))) # A DEFAULT WITH NO PRECEDING FIELD IS REFUSED. `__getitem__' needs a # `__last_field' to attach the default to. emit('default_without_field_error', errmsg(lambda: fields.Fields[1])) # DUNDER LOOKUP IS NOT A FIELD DECLARATION. `__getattr__' short-circuits names # that are dunders, so `Fields.__len__' raises AttributeError rather than # declaring a field called `__len__'. Without that, every protocol probe on the # factory would silently extend the declaration. emit('dunder_not_a_field', err(lambda: fields.Fields.__nonexistent__)) # ------------------------------------------------ Fields: the generated class --- # THE DOCUMENTED ENTRY POINT, and the shape every case in this section is about. class Pizza(fields.Fields.name.size): pass emit('pizza_name', Pizza.__name__) # SUBCLASSING SEALED IT: the base is the sealer's generated class. ITS NAME IS # THE BARE `FieldsBase', not the `FieldsBase_for__name__size' the sealer # computes -- `class_sealer' writes `class FieldsBase(base)' as a literal and # uses `baseclass_name' only as a key in the generated source's `super()' call. # So EVERY class this sealer produces shares one name, and a traceback through # two of them cannot be told apart by it. `tuple_sealer_result' below is the # contrast: that sealer builds its class with `type(baseclass_name, ...)' and # does carry the per-field name. emit('pizza_base_name', Pizza.__bases__[0].__name__) emit('pizza_base_count', str(len(Pizza.__bases__))) # AND ITS METACLASS IS PLAIN `type' AGAIN. `_Factory.__new__' with bases # present returns `type(name, ...)', so the sealed class is an ordinary class -- # a subclass of it does not re-enter the factory. emit('pizza_metaclass', type(Pizza).__name__) _p = Pizza("Pepperoni", "large") # POSITIONAL CONSTRUCTION BINDS IN DECLARATION ORDER. This is the claim the # whole `Fields.a.b' syntax rests on: the order of the attribute chain is the # order of the positional parameters, and nothing else records it. emit('pizza_pos_name', _p.name) emit('pizza_pos_size', _p.size) # KEYWORDS WORK TOO, and out of order. _pk = Pizza(size="large", name="Pepperoni") emit('pizza_kw_name', _pk.name) emit('pizza_kw_size', _pk.size) # THE GENERATED `__repr__' NAMES THE SUBCLASS, NOT THE GENERATED BASE, and it # prints every field as `name=' in declaration order. Pinned whole # because the field ORDER inside it is the same claim `pizza_pos_*' makes, # observed from the other side. emit('pizza_repr', repr(_p)) emit('pizza_str_is_repr', 'yes' if str(_p) == repr(_p) else 'no') # THE REPR USES `repr()' ON THE VALUES, not `str()'. So an int is bare and a # str is quoted -- which is what makes the output round-trippable by eye, and # what a `str()'-based release would silently break. emit('repr_uses_repr', repr(Pizza(1, "x"))) # THE FIELD NAMES AND VALUES ARE ALL PRESENT IN THE REPR. Asserted as a # derived boolean as well as by the literal above, because it is the ticket's # own requirement and should be one case a reader can find. _rp = repr(_p) emit('repr_has_all_fields', 'yes' if all(('%s=%r' % (k, getattr(_p, k))) in _rp for k in ('name', 'size')) else 'no') # --------------------------------------------------------------- __init__ --- # THE MISSING-FIELD AND UNEXPECTED-FIELD ERRORS. These come from the # INTERPRETER, not from this library -- the generated `__init__' is an ordinary # function with ordinary parameters, so a missing argument is CPython's own # TypeError. That is why the FUNCTION NAME is in the message, and why the name # `make_init_func' chose is worth pinning: it is what a user sees first. emit('missing_field_error', errmsg(Pizza, "Pepperoni")) emit('unexpected_field_error', errmsg(Pizza, "Pepperoni", "large", crust="thin")) emit('extra_positional_error', errmsg(Pizza, 1, 2, 3)) # THE GENERATED FUNCTION'S NAME, read off the sealed base rather than off a # traceback. `make_init_func' builds `__fields_init_for__name__size__' and then # rebinds it to `__init__', so `__name__' keeps the generated spelling. emit('init_func_name', Pizza.__bases__[0].__init__.__name__) # ITS SIGNATURE IS THE DECLARATION, in order, after `self'. try: import inspect emit('init_params', ','.join(list(inspect.signature(Pizza.__bases__[0].__init__) .parameters))) except BaseException as exc: emit('init_params', 'ERR:%s' % type(exc).__name__) # THE GENERATED SOURCE IS REGISTERED WITH `linecache', which is the only reason # a traceback through the generated `__init__' can show its source at all -- # there is no file behind it. The filename is `' with # an adler32 of the source, so it is CONTENT-derived and stable across runs and # arms. This is the case that would notice `linecache' being dropped from the # image: the generated class would still work and every traceback through it # would go blank. import linecache _initf = Pizza.__bases__[0].__init__.__code__.co_filename emit('init_filename', _initf) emit('init_in_linecache', 'yes' if _initf in linecache.cache else 'no') _src = ''.join(linecache.cache[_initf][2]) if _initf in linecache.cache else '' emit('init_source_lines', str(len(linecache.cache[_initf][2])) if _initf in linecache.cache else '0') emit('init_source_has_super', 'yes' if 'super(' in _src else 'no') # -------------------------------------------------------------- comparison --- # `__eq__' COMPARES THE FIELD TUPLE, IN ORDER, and is guarded by # `isinstance(other, self.__class__)'. Equal fields compare equal; one # differing field does not. _a = Pizza("Pepperoni", "large") _b = Pizza("Pepperoni", "large") _c = Pizza("Pepperoni", "small") emit('eq_equal_fields', 'yes' if _a == _b else 'no') emit('eq_differing_fields', 'yes' if _a == _c else 'no') emit('ne_differing_fields', 'yes' if _a != _c else 'no') emit('eq_is_not_identity', 'yes' if _a is not _b else 'no') # THE FULL ORDERING SET. `Fields' generates all six, and they compare the field # TUPLE -- so ordering is lexicographic by declaration order, which is a real # and easily-missed consequence of the syntax. emit('lt', 'yes' if _c < _a else 'no') # 'small' > 'large' is False emit('gt', 'yes' if _a < _c else 'no') emit('le_equal', 'yes' if _a <= _b else 'no') emit('ge_equal', 'yes' if _a >= _b else 'no') # COMPARISON WITH A FOREIGN TYPE RETURNS `NotImplemented', which Python then # turns into `False' for `==' and into a TypeError for `<'. Both halves pinned: # the delegation is what lets a subclass on the other side win, and the TypeError # is what a user actually meets. emit('eq_foreign', 'yes' if (_a == 42) else 'no') emit('lt_foreign_error', err(lambda: _a < 42)) # `__hash__' IS CONSISTENT WITH `__eq__' -- equal instances hash equal -- and it # is the hash of the same field tuple. The second case is the one that would # catch a `__hash__' left to identity, which would keep `hash_equal' failing and # `eq_equal_fields' green. emit('hash_equal', 'yes' if hash(_a) == hash(_b) else 'no') emit('hash_differs', 'yes' if hash(_a) != hash(_c) else 'no') emit('hash_is_tuple_hash', 'yes' if hash(_a) == hash(("Pepperoni", "large")) else 'no') # AND THE INSTANCES ARE USABLE AS DICT KEYS, which is the consequence that # matters and the one a broken `__hash__'/`__eq__' pair breaks in a way no # direct comparison shows. emit('hash_dedups', str(len({_a, _b, _c}))) # ---------------------------------------------------------------- defaults --- # THE KEYWORD-DEFAULT FORM, `Fields.value.left[None].right[None]' -- the # `__getitem__' half of the syntax. A defaulted field may be omitted, and the # defaults are captured ONCE at seal time into the generated function's # defaults, not re-evaluated per call. class Node(fields.Fields.value.left[None].right[None]): pass _n = Node(1) emit('node_repr', repr(_n)) emit('node_defaults_left', repr(_n.left)) _n2 = Node(1, right=Node(2)) emit('node_mixed_repr', repr(_n2)) emit('node_init_defaults', repr(Node.__bases__[0].__init__.__defaults__)) # A DEFAULT IS SHARED, NOT COPIED. `make_init_func' puts the default object # into the function's `__defaults__', so a MUTABLE default is the classic shared # one -- two instances that omit it get the SAME list. Pinned because this # library's syntax makes `[[]]' look like a per-instance empty list. class Bag(fields.Fields.items[[]]): pass _b1, _b2 = Bag(), Bag() emit('mutable_default_shared', 'yes' if _b1.items is _b2.items else 'no') # ----------------------------------------------------------- the inline form --- # A FACTORY IS CALLABLE WITHOUT SUBCLASSING. `_Factory.__call__' does `(~cls)(...)', # so `Fields.name.size("a", "b")' seals and instantiates in one step -- and the # resulting repr names the GENERATED base, since there is no subclass to name. _inline = fields.Fields.name.size("Pepperoni", "large") emit('inline_repr', repr(_inline)) emit('inline_class_name', type(_inline).__name__) # SEALING IS MEMOISED PER FACTORY. `__invert__' caches into `__concrete', so # calling one factory twice yields the same class -- while two separately-built # chains with the same fields do not. _f = fields.Fields.x.y emit('seal_memoised', 'yes' if (~_f) is (~_f) else 'no') emit('seal_not_shared_across_chains', 'yes' if (~fields.Fields.x.y) is (~fields.Fields.x.y) else 'no') # ------------------------------------------------------------------- Tuple --- # THE `namedtuple' ALTERNATIVE. `tuple_sealer' builds a real `tuple' subclass # with `itemgetter' properties, so the fields are POSITIONS rather than instance # attributes -- which is why it is the one variant with no instance `__dict__'. class Pair(fields.Tuple.a.b): pass _pr = Pair(1, 2) emit('tuple_issubclass', 'yes' if issubclass(Pair, tuple) else 'no') emit('tuple_a', str(_pr.a)) emit('tuple_b', str(_pr.b)) emit('tuple_as_tuple', str(tuple(_pr))) emit('tuple_len', str(len(_pr))) emit('tuple_index', str(_pr[0])) emit('tuple_unpacks', str(list(_pr))) emit('tuple_repr', repr(_pr)) # THE ATTRIBUTES ARE `property' OBJECTS ON THE GENERATED BASE, built from # `operator.itemgetter'. That is what the ticket's "asserted for the attributes # they expose" means here, and it is asserted by TYPE as well as by value: a # release that switched to plain slots would keep every value case green. emit('tuple_attr_is_property', 'yes' if isinstance(type(_pr).__mro__[1].__dict__.get('a'), property) else 'no') # A FIELD CANNOT BE REBOUND -- but NOT because the instance is slotted. The # generated base carries `__slots__ = ()' and the refusal is the read-only # `property': `setattr' hits a descriptor with no setter and raises # AttributeError. # # THE INSTANCE STILL HAS A `__dict__', and that is the finding. `Pair' -- the # user's own subclass -- declares no `__slots__' of its own, and unlike # `SlotsFields' this sealer installs no metaclass to force one, so Python gives # it a dict. An UNDECLARED attribute therefore assigns fine. So `Tuple' buys # the tuple's packed storage for the FIELDS and none of the memory saving people # reach for `__slots__' for; contrast `slots_has_dict' below, which is `no'. emit('tuple_assign_error', err(setattr, _pr, 'a', 9)) emit('tuple_has_dict', 'yes' if hasattr(_pr, '__dict__') else 'no') emit('tuple_undeclared_attr_ok', err(setattr, _pr, 'zz', 9)) emit('tuple_base_slots', str(type(_pr).__mro__[1].__slots__)) # EQUALITY AND HASH ARE `tuple''S OWN, not generated -- so a Pair equals a plain # tuple with the same contents, which is NOT true of any other variant here. # Pinned because it is the one place `Tuple' diverges from the ticket's "two # instances with equal fields compare equal" in a direction worth knowing. emit('tuple_eq_plain_tuple', 'yes' if _pr == (1, 2) else 'no') emit('tuple_eq_same_fields', 'yes' if Pair(1, 2) == Pair(1, 2) else 'no') emit('tuple_hash_equal', 'yes' if hash(Pair(1, 2)) == hash((1, 2)) else 'no') # `__getnewargs__' IS WHAT MAKES IT PICKLE, and it is the only reason a # `tuple' subclass with a custom `__new__' survives a round trip. # `__getnewargs__' IS WHAT MAKES IT PICKLE, and it is the only reason a `tuple' # subclass with a custom `__new__' survives a round trip. What is asserted is # that it returns the field tuple: an actual round trip is not, because the # class is defined in this probe's `__main__' and pickle resolves a class by # NAME -- the failure would be about where the class lives, not about the # package. emit('tuple_getnewargs', str(_pr.__getnewargs__())) emit('tuple_new_name', type(_pr).__mro__[1].__new__.__name__) # ------------------------------------------------------------- SlotsFields --- # THE SLOTS VARIANT. `slots_class_sealer' puts a metaclass on the generated # base that FORCES `__slots__' onto any subclass that does not declare one -- # which is what makes the saving survive subclassing, and is the whole point. class Point(fields.SlotsFields.x.y): pass _pt = Point(1, 2) emit('slots_x', str(_pt.x)) emit('slots_y', str(_pt.y)) emit('slots_repr', repr(_pt)) # THE SUBCLASS GOT `__slots__' IT NEVER DECLARED, naming exactly the fields. emit('slots_subclass_slots', ','.join(Point.__slots__)) emit('slots_meta_name', type(Point).__name__) # AND SO THERE IS NO INSTANCE `__dict__': an undeclared attribute is refused. # This is the case that separates `SlotsFields' from `Fields' in one reading. emit('slots_has_dict', 'yes' if hasattr(_pt, '__dict__') else 'no') emit('slots_new_attr_error', err(setattr, _pt, 'z', 3)) # A DECLARED FIELD IS STILL WRITABLE -- slots restrict which names exist, not # whether they can be rebound. _pt.x = 9 emit('slots_field_writable', str(_pt.x)) # AND IT STILL COMPARES AND PRINTS: `slots_class_sealer' delegates to # `class_sealer', so the whole comparison set is inherited. emit('slots_eq', 'yes' if Point(1, 2) == Point(1, 2) else 'no') emit('slots_hash_equal', 'yes' if hash(Point(1, 2)) == hash(Point(1, 2)) else 'no') # ------------------------------------------------------------- other factories --- # `BareFields' -- `__init__' ONLY. No comparison and no repr, so two equal # instances are NOT equal and the repr is the default `<... object at 0x...>'. # Asserted as the ABSENCE of the generated methods rather than by repr'ing the # object, which would carry an address. class Bare(fields.BareFields.a.b): pass _bare = Bare(1, 2) emit('bare_a', str(_bare.a)) emit('bare_eq', 'yes' if Bare(1, 2) == Bare(1, 2) else 'no') emit('bare_repr_is_default', 'yes' if repr(_bare).startswith('<') and ' object at 0x' in repr(_bare) else 'no') # `ConvertibleFields' -- adds `as_dict' and `as_tuple' PROPERTIES on top of the # full `Fields' set. The dict is a plain `dict' in field order; the tuple is the # same tuple `__eq__' and `__hash__' compare. class Conv(fields.ConvertibleFields.a.b): pass _cv = Conv(1, 2) emit('conv_as_dict', str(_cv.as_dict)) emit('conv_as_tuple', str(_cv.as_tuple)) emit('conv_as_dict_type', type(_cv.as_dict).__name__) emit('conv_still_comparable', 'yes' if Conv(1, 2) == Conv(1, 2) else 'no') # THE MIXINS -- `initializer=False', so they generate NO `__init__' and are # meant to be combined with a field-bearing base. This is the documented # "extend rather than replace" form: `BareFields' supplies the fields and the # initializer, the mixin supplies methods over a SUBSET of them. class Mixed(fields.BareFields.a.b.c, fields.ComparableMixin.a): pass # COMPARES ON `a' ALONE, though it carries three fields. Two instances agreeing # only on `a' are equal -- which is the mixin doing exactly what it says and is # also the sharpest way to show that the mixin's field list is its own. emit('mixin_eq_on_subset', 'yes' if Mixed(1, 2, 3) == Mixed(1, 3, 4) else 'no') emit('mixin_ne_on_subset', 'yes' if Mixed(1, 2, 3) != Mixed(2, 2, 3) else 'no') emit('mixin_carries_all_fields', ','.join(str(getattr(Mixed(1, 2, 3), n)) for n in ('a', 'b', 'c'))) emit('mixin_base_count', str(len(Mixed.__bases__))) class Printed(fields.BareFields.a.b.c, fields.PrintableMixin.a.b): pass # PRINTS A SUBSET TOO, and names the subclass. `test_printable' upstream # asserts exactly this string. emit('printable_mixin_repr', repr(Printed(1, 2, 3))) # ----------------------------------------------------- InheritableFields --- # THE MULTIPLE-INHERITANCE FORM, and the ticket's "inheritance extends the field # list rather than replaces it" in its strongest reading. `InheritableFields' # is `class_sealer' with `pass_kwargs=True' and `base=object', so its generated # `__init__' takes `**__fields_kwargs__' and passes them UP -- which is what # lets two independently-declared field sets cooperate in one MRO. # # IT IS NOT IN `__all__' (see `all_missing_public' above), so `from fields # import *' does not bind the only factory that does this. class _A(fields.InheritableFields.name): pass class _B(fields.InheritableFields.age): pass class Person(_A, _B): pass _person = Person(name='hans', age='43') emit('inherit_name', _person.name) emit('inherit_age', _person.age) emit('inherit_field_count', str(sum(1 for n in ('name', 'age') if hasattr(_person, n)))) # AN UNDECLARED KEYWORD IS STILL REFUSED, which is what says the extension is a # real union of the two field lists rather than a `**kwargs' that swallows # anything. emit('inherit_bogus_error', err(Person, name='hans', age='43', bogus='x')) emit('inherit_mro_len', str(len(Person.__mro__))) # THE PLAIN `Fields' FACTORY DOES *NOT* SUPPORT THIS. Its generated `__init__' # passes only its own fields to `super()', so the second base's initializer # never gets its argument. Pinned as the contrast: it is why # `InheritableFields' exists, and a reader who reaches for `Fields' here needs # to meet the error rather than guess. class _C(fields.Fields.name): pass class _D(fields.Fields.age): pass class Broken(_C, _D): pass emit('plain_multiple_inheritance_error', err(Broken, name='hans', age='43')) # A CONFLICTING FIELD LAYOUT IS REFUSED AT CLASS-CREATION TIME, not at # construction. `_Factory.__new__' zips the bases' field lists position by # position and raises when a position carries two different names -- so # combining two factories whose fields disagree in order is a definition-time # error. def _conflict(): class _X(fields.BareFields.a.b, fields.ComparableMixin.b): pass emit('layout_conflict_error', errmsg(_conflict)) # ------------------------------------------------------ a custom __init__ --- # THE FEATURE THE README LEADS WITH: unlike `attrs' and `characteristic', the # subclass owns its own `__init__' and cooperates through `super()'. Both # halves asserted -- the custom body runs, and the generated one still binds # the fields. class Checked(fields.Fields.a.b): def __init__(self, a, b): if a > b: raise ValueError("Expected %s < %s" % (a, b)) super(Checked, self).__init__(a, b) emit('custom_init_ok', repr(Checked(1, 2))) emit('custom_init_raises', err(Checked, 2, 1)) # AND A CUSTOM `__init__' MAY TAKE ARGUMENTS OF ITS OWN, passing only the # declared fields up as keywords. class WithArg(fields.Fields.a): def __init__(self, arg, **kwargs): super(WithArg, self).__init__(**kwargs) self.arg = arg _wa = WithArg(42, a=1) emit('custom_init_extra_arg', '%s,%s' % (_wa.arg, _wa.a)) # ---------------------------------------------------- make_init_func direct --- # THE LOW-LEVEL ENTRY, exported in `__all__' and callable on its own. It is # also where the one non-TypeError refusal in this library lives: a POSITIONAL # field declared AFTER a defaulted one is a `ValueError' with a message naming # the offending field. The factory refuses that earlier and differently # (`required_after_default_error' above), so both paths are pinned. emit('make_init_func_error', errmsg(fields.make_init_func, ['a', 'b', 'c'], {'b': 1})) _g, _l = fields.make_init_func(['a', 'b'], {}) emit('make_init_func_keys', ','.join(sorted(_l))) emit('make_init_func_global_keys', ','.join(sorted(_g))) # ------------------------------------------------------------- the sealers --- # THREE SEALERS ARE EXPORTED and each is an ordinary function taking # `(fields, defaults)'. Asserted by name and by the class each produces, so a # release that renamed one is a red here rather than an AttributeError inside # somebody's `factory()' call. emit('sealer_names', ','.join(n for n in ('class_sealer', 'slots_class_sealer', 'tuple_sealer') if callable(getattr(fields, n, None)))) # THE TWO SEALERS NAME THEIR CLASSES DIFFERENTLY, and this pair is where that # is stated: `class_sealer' returns the bare `FieldsBase' (see `pizza_base_name' # above), `tuple_sealer' returns `FieldsBase_for__a__b'. Same computed # `baseclass_name', used by one and discarded by the other. emit('class_sealer_result', fields.class_sealer(['a', 'b'], {}).__name__) emit('tuple_sealer_result', fields.tuple_sealer(['a', 'b'], {}).__name__) # `factory()' BUILDS A NEW FACTORY FROM ANY SEALER, which is the documented # extension point. A sealer here is called with the field list IN ORDER and the # defaults as an `OrderedDict'. _seen = {} def _spy(flds, defaults): _seen['fields'] = list(flds) _seen['defaults'] = list(defaults.items()) if defaults else [] return object class _Spied(fields.factory(_spy).foo.bar.lorem[1].ipsum[2]): pass emit('factory_sealer_fields', ','.join(_seen.get('fields', []))) emit('factory_sealer_defaults', ','.join('%s=%r' % kv for kv in _seen.get('defaults', []))) emit('factory_sealer_base', _Spied.__bases__[0].__name__) # ------------------------------------------------------------- Namespace --- # A BACKPORT OF `types.SimpleNamespace', carried because this package still # supported 2.6 -- dead weight on 3.12 and shipped anyway. Its `__repr__' sorts # the keys, which `SimpleNamespace' does NOT, so the two are not # interchangeable in output. _ns = fields.Namespace(b=2, a=1) emit('namespace_repr', repr(_ns)) emit('namespace_eq', 'yes' if fields.Namespace(a=1) == fields.Namespace(a=1) else 'no') # IT DEFINES `__eq__' WITHOUT `__hash__', so Python sets `__hash__ = None' and # the instances are UNHASHABLE. A real finding rather than a detail: it is the # one object in this package that cannot go in a set. emit('namespace_hashable', 'yes' if _ns.__hash__ is not None else 'no') emit('namespace_hash_error', err(hash, _ns)) # AND ITS `__eq__' IS UNGUARDED -- it reads `other.__dict__' with no isinstance # check, so comparing to an int raises rather than returning NotImplemented. emit('namespace_eq_foreign_error', err(lambda: _ns == 42)) # --------------------------------------------------------------- extras --- # THE SEPARATE SUBMODULE, imported here for the first time (see # `extras_autoimported' above). It is the package's own worked example of a # custom sealer and is what `regex_validation_sealer' is for. try: from fields import extras emit('extras_import_ok', 'yes') emit('extras_error', '') except BaseException as exc: extras = None emit('extras_import_ok', 'no') emit('extras_error', '%s: %s' % (type(exc).__name__, str(exc).replace('\n', ' ')[:200])) if extras is not None: emit('extras_names', ','.join(sorted(n for n in vars(extras) if not n.startswith('_')))) # `ValidationError' IS ITS OWN EXCEPTION and derives from `Exception'. emit('extras_validation_error_base', extras.ValidationError.__bases__[0].__name__) class Wants(extras.RegexValidate.value['aa+']): pass # A MATCHING VALUE IS ACCEPTED and a non-matching one raises. The # validation happens in the sealer's `__init__' BEFORE `super()', so the # attribute is never set on a rejected instance. emit('regex_accepts', err(Wants, 'aaa')) emit('regex_rejects', err(Wants, 'b')) # THE SEALER REFUSES REQUIRED FIELDS ALTOGETHER -- every field must carry # its pattern as a default, and a bare `RegexValidate.value' is a # definition-time TypeError. def _bare_regex(): class _Y(extras.RegexValidate.value): pass emit('regex_requires_defaults', err(_bare_regex)) # COMBINED WITH `Fields', the validator runs and the fields are still set -- # the documented composition, and upstream's own `test_regex_validation'. class Both(extras.RegexValidate.value['aa+'], fields.Fields.value): pass emit('regex_combined_ok', repr(Both('aaa'))) emit('regex_combined_rejects', err(Both, 'b')) else: for _k in ('extras_names', 'extras_validation_error_base', 'regex_accepts', 'regex_rejects', 'regex_requires_defaults', 'regex_combined_ok', 'regex_combined_rejects'): emit(_k, 'skipped') # ------------------------------------------------------------- interpreter --- # WHICH ARM THIS TRANSCRIPT IS FROM, so that a control run and a target run are # distinguishable in the capture without reading the surrounding log. # # `py_maxsize_bits' is the ONE value in this file the suite does not compare # against the control: it reads 64 there and must read 32 here. Without it # every other value here is arm-independent by construction, so a suite run # against a HOST copy of the package would pass in full. emit('py_version', '%d.%d' % sys.version_info[:2]) emit('py_maxsize_bits', str(sys.maxsize.bit_length() + 1)) emit('emitted_count', str(len(emitted) + 2)) emit('probe_done', 'yes') sys.exit(0)