# python3-pretend probe. ONE interpreter start, every measurement below. # # Emits `key=value' lines on stdout. The suite reads them by key, so a case # added here without a reader in the suite is inert rather than fatal, and a key # the suite reads but the probe never emits reads as the empty string -- which # `probe_done' and `emitted_count' are what catch. # # EXITS 0 WHATEVER HAPPENS. A probe that exited non-zero on an import failure # would turn one red into a probe overrun and cost the transcript, so the import # result is reported as a VALUE (`import_ok') and never as a status. # # NOTHING HERE TOUCHES THE FILESYSTEM. pretend is a stubbing library: every # fixture it needs is an object this file constructs in memory. No temporary # directory, no subprocess, no socket, no device, no terminal -- which is why # this probe is the whole behavioural half of the suite and why its budget is # the ordinary one rather than vcversioner's enlarged one. import functools import sys import traceback _emitted = [] def emit(key, value): _emitted.append(key) sys.stdout.write("%s=%s\n" % (key, value)) def yn(b): return "yes" if b else "no" def flat(s): """One line, so a value carrying newlines stays one counted result.""" return s.replace("\r", "").replace("\n", "|") def guard(key, fn): """Emit fn()'s value, or `RAISED:' if it raised. One probe case that throws must not take the remaining cases down with it.""" try: emit(key, fn()) except Exception as e: emit(key, "RAISED:%s" % type(e).__name__) def raises(fn): """`' if fn() raised, `NONE:' if it returned. The library's whole subject is behaviour under a collaborator that misbehaves, so `what did NOT raise' has to be as legible as `what did'.""" try: r = fn() except BaseException as e: return type(e).__name__ return "NONE:%r" % (r,) # --------------------------------------------------------------- import --- try: import pretend emit("import_ok", "yes") except Exception: emit("import_ok", "no") emit("import_error", flat(traceback.format_exc().strip())) emit("probe_done", "yes") emit("emitted_count", len(_emitted) + 1) sys.exit(0) emit("module_file", getattr(pretend, "__file__", "")) emit("module_name", pretend.__name__) # IT IS A SINGLE MODULE, NOT A PACKAGE. setup.py declares `py_modules=' rather # than `packages=', so there is no `__path__' and no directory payload. emit("is_package", yn(hasattr(pretend, "__path__"))) # The module's whole non-underscore surface, including the two stdlib names it # imports at module level -- `functools' and `sys' are reachable as # `pretend.functools' and a reader should be able to see that from the suite. emit("public_names", ",".join(sorted( n for n in dir(pretend) if not n.startswith("_")))) # NO `__version__' AND NO `__all__'. Both asserted as ABSENCES: the version # lives only in the dist-info. emit("has_version_attr", yn(hasattr(pretend, "__version__"))) emit("has_all", yn(hasattr(pretend, "__all__"))) emit("stub_is_class", yn(isinstance(pretend.stub, type))) emit("call_is_class", yn(isinstance(pretend.call, type))) emit("recorder_callable", yn(callable(pretend.call_recorder))) emit("raiser_callable", yn(callable(pretend.raiser))) # `PY3K' is a module-level constant computed from sys.version_info, and it # selects which two names go into MAGIC_METHODS. Asserted because the whole # magic-method table below is downstream of it. emit("py3k", yn(pretend.PY3K)) # THE MAGIC-METHOD TABLE IS A WHITELIST, AND IT IS THE ONE PIECE OF DATA IN # THIS LIBRARY. A kwarg whose name is not in it is stored as an ordinary # attribute and is NEVER dispatched -- `__str__' below is the case that proves # it -- so the table's exact membership is what separates a working stub from # one that silently ignores the method a test supplied. emit("magic_count", len(pretend.MAGIC_METHODS)) emit("magic_is_frozenset", yn(isinstance(pretend.MAGIC_METHODS, frozenset))) emit("magic_methods", ",".join(sorted(pretend.MAGIC_METHODS))) # The py2-only names must be ABSENT under PY3K, and the py3-only names present. emit("magic_has_next", yn("__next__" in pretend.MAGIC_METHODS)) emit("magic_has_bool", yn("__bool__" in pretend.MAGIC_METHODS)) emit("magic_has_div", yn("__div__" in pretend.MAGIC_METHODS)) emit("magic_has_nonzero", yn("__nonzero__" in pretend.MAGIC_METHODS)) # `methods', the module-level set the table is built from, is DELETED at the end # of the build. Asserted so the module's namespace stays the audited one. emit("methods_leaked", yn(hasattr(pretend, "methods"))) # --------------------------------------------------------- stub: shape --- # THE CENTRAL PROPERTY, AND THE ONE A PORT MUST NOT LOSE: the attributes of a # stub are EXACTLY the keyword arguments it was given, of the values it was # given -- and an attribute that was not declared raises AttributeError rather # than being auto-created. That is what separates this library from a permissive # mock, which would answer anything and let a test pass over a collaborator it # had misspelled. _s = pretend.stub(a=1, b="x", c=[1, 2]) emit("stub_a", _s.a) emit("stub_b", _s.b) emit("stub_c", _s.c) emit("stub_c_is_same_object", yn(_s.c is _s.__dict__["c"])) emit("stub_undeclared", raises(lambda: _s.d)) # A DUNDER that was not declared is refused the same way -- there is no # fallback that would make an undeclared magic method appear. emit("stub_undeclared_dunder", raises(lambda: _s.__len__)) emit("stub_dict", ",".join("%s=%r" % kv for kv in sorted(_s.__dict__.items()))) emit("stub_dict_len", len(_s.__dict__)) # A stub is an ORDINARY object otherwise: no __slots__, so an attribute can be # added after construction. Asserted because it bounds the claim above -- the # AttributeError is about what was declared at construction, not a freeze. def _case_stub_setattr(): s = pretend.stub(a=1) s.b = 2 return s.b guard("stub_setattr", _case_stub_setattr) # A CALLABLE ATTRIBUTE IS CALLED WITHOUT A SELF. It is stored in the instance # __dict__, so the descriptor protocol never runs on it and it is not bound -- # a plain lambda of the arity the test wrote is what is called. emit("stub_callable_attr", pretend.stub(f=lambda: 7).f()) emit("stub_callable_attr_args", pretend.stub(f=lambda x, y: x + y).f(2, 3)) emit("stub_callable_not_bound", raises(lambda: pretend.stub(f=lambda self: self).f())) emit("stub_empty_repr", repr(pretend.stub())) emit("stub_repr", repr(pretend.stub(a=1))) # INSERTION ORDER, not sorted order. __repr__ walks self.__dict__, and a dict # has preserved insertion order since 3.7 -- so the literal a reader writes in a # test is the literal they get back, and kwargs order is what decides it. emit("stub_repr_two", repr(pretend.stub(a=1, b=2))) emit("stub_repr_two_reversed", repr(pretend.stub(b=2, a=1))) emit("stub_repr_uses_repr", repr(pretend.stub(a="x"))) # EVERY STUB IS AN INSTANCE OF A GENERATED SUBCLASS, NEVER OF `stub' ITSELF. # __new__ builds a subclass carrying the dispatchers for the magic methods this # call declared, and caches it on `stub._classes_cache' keyed by that frozenset. # So `type(s) is pretend.stub' is FALSE even for a stub with no magic methods at # all, while `isinstance(s, pretend.stub)' is true -- a distinction a reader is # likely to get the wrong way round, and one an isinstance check in a test rests # on. _plain = pretend.stub(a=1) emit("stub_type_name", type(_plain).__name__) emit("stub_type_module", type(_plain).__module__) emit("stub_type_is_stub", yn(type(_plain) is pretend.stub)) emit("stub_isinstance", yn(isinstance(_plain, pretend.stub))) emit("stub_type_base_is_stub", yn(type(_plain).__bases__ == (pretend.stub,))) # THE SUBCLASS CACHE IS KEYED BY THE SET OF MAGIC METHODS AND BY NOTHING ELSE. # Two stubs with different ordinary attributes share one generated class; two # with different magic-method sets do not. That is a per-process cache on the # class, so it is also the one piece of state this library keeps between calls. emit("stub_cache_shared_type", yn(type(pretend.stub(a=1)) is type(pretend.stub(z=9)))) emit("stub_cache_magic_type_differs", yn(type(pretend.stub(__len__=lambda: 1)) is type(pretend.stub(a=1)))) emit("stub_cache_same_magic_shared", yn(type(pretend.stub(__len__=lambda: 1)) is type(pretend.stub(__len__=lambda: 2)))) emit("stub_cache_is_dict", yn(isinstance(pretend.stub._classes_cache, dict))) emit("stub_cache_key_frozenset", yn(all(isinstance(k, frozenset) for k in pretend.stub._classes_cache))) emit("stub_cache_has_empty_key", yn(frozenset() in pretend.stub._classes_cache)) # ---------------------------------------------------- stub: dispatchers --- # # A magic method supplied as a kwarg is reachable through the SYNTAX, not merely # as an attribute: python looks a dunder up on the type, and the generated # subclass carries a dispatcher that forwards to self.__dict__[name]. Each case # below goes through the operator or builtin rather than calling the attribute, # because calling the attribute would pass with no dispatcher at all. guard("magic_len", lambda: len(pretend.stub(__len__=lambda: 3))) guard("magic_getitem", lambda: pretend.stub(__getitem__=lambda k: k * 2)[21]) def _setitem(): seen = [] s = pretend.stub(__setitem__=lambda k, v: seen.append((k, v))) s["k"] = "v" return "%s=%s" % seen[0] guard("magic_setitem", _setitem) def _delitem(): seen = [] s = pretend.stub(__delitem__=lambda k: seen.append(k)) del s["k"] return seen[0] guard("magic_delitem", _delitem) guard("magic_iter", lambda: ",".join( str(x) for x in pretend.stub(__iter__=lambda: iter([1, 2, 3])))) guard("magic_contains", lambda: yn(2 in pretend.stub(__contains__=lambda x: x == 2))) guard("magic_call", lambda: pretend.stub(__call__=lambda x: x * 2)(5)) guard("magic_bool", lambda: yn(bool(pretend.stub(__bool__=lambda: False)))) guard("magic_next", lambda: next(pretend.stub(__next__=lambda: 42))) guard("magic_add", lambda: pretend.stub(__add__=lambda o: "add:%s" % o) + 1) guard("magic_mul", lambda: pretend.stub(__mul__=lambda o: o * 3) * 4) guard("magic_sub", lambda: pretend.stub(__sub__=lambda o: 10 - o) - 4) guard("magic_lt", lambda: yn(pretend.stub(__lt__=lambda o: True) < 0)) guard("magic_eq", lambda: yn(pretend.stub(__eq__=lambda o: o == "yes") == "yes")) guard("magic_ne", lambda: yn(pretend.stub(__ne__=lambda o: True) != 0)) guard("magic_repr", lambda: repr(pretend.stub(__repr__=lambda: ""))) # The context-manager pair, through the `with' statement itself. def _case_ctx(): seen = [] s = pretend.stub(__enter__=lambda: "resource", __exit__=lambda *a: seen.append(len(a))) with s as r: got = r return "%s,%s" % (got, seen[0]) guard("magic_context", _case_ctx) # __exit__ IS HANDED THREE ARGUMENTS EVEN ON THE CLEAN PATH -- (None, None, # None) -- so a stub whose __exit__ takes no arguments raises TypeError at the # end of the block rather than at its start. That is the arity mistake this # library's users make, and it is a dispatcher fact rather than a python one. def _exit_arity(): with pretend.stub(__enter__=lambda: 1, __exit__=lambda: None): pass emit("magic_exit_arity", raises(_exit_arity)) # THE DISPATCHER PASSES NO SELF. `_build_magic_dispatcher' forwards # `self.__dict__[method](*args, **kwargs)', so the supplied callable sees the # operator's arguments and nothing else -- `__len__=lambda self: 3' is a # TypeError, not a working stub. Both directions are asserted because either # alone is equally consistent with an ordinary bound method. emit("magic_no_self", raises(lambda: len(pretend.stub(__len__=lambda s: 3)))) # The dispatcher is renamed to the method it serves, which is what makes a # traceback through a stub readable. emit("magic_dispatcher_name", type(pretend.stub(__len__=lambda: 1)).__len__.__name__) # ...and it lives on the TYPE while the supplied callable stays in the instance # __dict__, so reading the attribute gives back the raw callable rather than the # dispatcher. def _case_attr_is_raw(): f = lambda: 3 return yn(pretend.stub(__len__=f).__len__ is f) guard("magic_attr_is_raw", _case_attr_is_raw) # A DUNDER OUTSIDE THE TABLE IS NOT DISPATCHED. `__str__' is not in # MAGIC_METHODS, so supplying it stores an attribute nothing consults and # `str()' falls back to `__repr__' -- which IS in the table and is the default # one here. A stub that silently ignored the method a test supplied is the # failure this asserts against. # Compared against this stub's OWN repr rather than against a literal: the # fallback text carries the stored lambda's address, which is not reproducible # between runs and would make the case a coin toss rather than an assertion. def _case_not_in_table_str(): f = lambda: "mine" s = pretend.stub(__str__=f) return "%s,%s,%s" % (yn(str(s) == repr(s)), yn(s.__dict__["__str__"] is f), yn(str(s).startswith("