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

Help numerary help @beartype (help numerary) by porting CachingProtocolMeta to beartype #8

Closed
posita opened this issue Jan 20, 2022 · 10 comments · Fixed by beartype/beartype#86

Comments

@posita
Copy link
Collaborator

posita commented Jan 20, 2022

Original proposal attributed to @leycyc. From #7 (comment):

Diversionary trainwreck alert: you have implemented something suspiciously fast and dangerously beautiful with the homegrown private caching protocol API (HPCPAPI) that is the beating heart of numerary. Would incorporating that API directly into beartype itself be feasible?

Here is what I am cogitating. typing.Protocol is slower than good ol' Southern molasses. beartype.typing.Protocol is currently just a trivial alias for typing.Protocol and thus also slower than that foodstuff. beartype.typing.Protocol doesn't need to be a trivial alias, however; in theory, beartype.typing.Protocol could be redefined to be your HPCPAPI. Every problem is now solved. Structural subtyping is now genuinely usable.

There's a caveat. If caching dramatically increases space complexity for downstream consumers in unexpected ways, the userbase will abandon @beartype after doxxing me on 4chan. That's bad. You've probably already tackled this concern (...maybe with an internal LRU cache?). If so, I have no remaining concerns and (with your generous permission and all the glory attributed directly to you as I abase myself before your complex throne of head-spinning protocols) will shortly assimilate your HPCPAPI into beartype.typing.Protocol. That's good.

Filing separately because #7 deals with a broader issue, and I got distracted by this shiny object.

@posita
Copy link
Collaborator Author

posita commented Jan 20, 2022

I love it! I have not experimented with an LRU cache because my focus was limited to to number types, which tends to translate to a small number of cached protocols. (I also assumed that one is not computing tensor densities¹ on Raspberry Pis.)

That said, I could certainly tinker around with the idea and see if it resulted in any significant difference in performance.

Either way, I would be honored to contribute to a beartype feature in this way. Let's do it! If you have a solid idea about the details, I can help be along for the ride. If you haven't gotten that far, I can tackle a (rant-free²) PR as a starting point.

First discussion topic: Should these be available in Python 3.7?

My recommendation is: Nope, >=3.8 only. (Read on for my reasoning.)

The reason I ask is that I think there is a conditional dependency on typing_extensions for version <3.8 at least to get Protocol and runtime_checkable. I started looking at beartype/typing.py and noticed that runtime_checkable only gets imported for >=3.8. In re-reading beartype/beartype#7, recalling various conversations, and glossing over beartype's README.rst, my understanding of beartype's philosophy is to do what it can for various Python versions, falling back to less cool stuff where needed, but without imposing additional runtime dependencies. (Apologies if dropping support for 3.7 is made explicit somewhere. I couldn't remember if, much less where.)

If that's accurate, I think we have (at least) two options.

Option 1 - lean on typing_extensions if it's available at runtime, but don't impose it as a requirement

# …
if _IS_PYTHON_AT_LEAST_3_8:
  # …
else:  # <-- this would be new
  try:
    from typing_extensions import Protocol, runtime_checkable
    _protocols_supported = True
  except ImportError:
    _protocols_supported = False
# …
if _protocols_supported:
  if TYPE_CHECKING:
    # Warning: Deep typing voodoo ahead. See
    # <https://github.com/python/mypy/issues/11614>.
    from abc import ABCMeta as _ProtocolMeta
  else:
    _ProtocolMeta = type(Protocol)

  class CachingProtocolMeta(_ProtocolMeta):
    # …
# …

Option 2 - no caching protocols for Python <3.8

# …
if _IS_PYTHON_AT_LEAST_3_8:
  # …  # <-- normal imports from typing, including Protocol
  if TYPE_CHECKING:
    # Warning: Deep typing voodoo ahead. See
    # <https://github.com/python/mypy/issues/11614>.
    from abc import ABCMeta as _ProtocolMeta
  else:
    _ProtocolMeta = type(Protocol)

  class CachingProtocolMeta(_ProtocolMeta):
    # …
# …

My preference is Option 2 as simpler, more consistent with the way that beartype handles capabilities elsewhere. Option 1 may lead to side-effects and probably imposes a more weighty testing requirement for <3.8 to ensure it's not engaged in false advertising.

UPDATE: Option 3 - beartype.typing.Protocol is the caching version

# …
if _IS_PYTHON_AT_LEAST_3_8:
  # …  # <-- normal imports from typing, including runtime_checkable, but not Protocol
  from typing import Protocol as _Protocol
  if TYPE_CHECKING:
    # Warning: Deep typing voodoo ahead. See
    # <https://github.com/python/mypy/issues/11614>.
    from abc import ABCMeta as _ProtocolMeta
  else:
    _ProtocolMeta = type(_Protocol)

  class ProtocolMeta(_ProtocolMeta):
    # …

  @runtime_checkable
  class Protocol(_Protocol, metaclass=ProtocolMeta):
    __slots__: Union[str, Iterable[str]] = ()
    # not sure what else--if anything--goes here
# …

After reading your #7 (comment) more carefully, I think this was your intent. This is probably easiest thing, assuming we do what beartype already does, which is not include beartype.typing.Protocol of any flavor for Python <3.8.

Thoughts?

Second discussion topic: What about manual overrides?

I (perhaps mis)identified a use case in numerary for a caller's ability to override isinstance results by manipulating the cache. Here's an excerpt from numerary's stream-of-consciousness README:

Remember that scandal where complex defined exception-throwing comparators it wasn’t supposed to have, which confused runtime protocol checking, and then its type definitions lied about it to cover it up?
Yeah, that shit ends here.

>>> from numerary.types import SupportsRealOps
>>> isinstance(1.0, SupportsRealOps)  # all good
True
>>> has_real_ops: SupportsRealOps = complex(1)  # type: ignore [assignment]  # properly caught by Mypy
>>> isinstance(complex(1), SupportsRealOps)  # you're not fooling anyone, buddy
False

numerary not only caches runtime protocol evaluations, but allows overriding those evaluations when the default machinery gets it wrong.

>>> from abc import abstractmethod
>>> from typing import Iterable, Union
>>> from numerary.types import CachingProtocolMeta, Protocol, runtime_checkable

>>> @runtime_checkable
... class MySupportsOne(Protocol, metaclass=CachingProtocolMeta):
...   __slots__: Union[str, Iterable[str]] = ()
...   @abstractmethod
...   def one(self) -> int:
...     pass

>>> class Imposter:
...   def one(self) -> str:
...     return "one"

>>> imp: MySupportsOne = Imposter()  # type: ignore [assignment]  # properly caught by Mypy
>>> isinstance(imp, MySupportsOne)  # fool me once, shame on you ...
True

>>> MySupportsOne.excludes(Imposter)
>>> isinstance(imp, MySupportsOne)  # ... can't get fooled again
False

numerary has default overrides to correct for known oddities with native types (like our old friend, complex) and with popular libraries like numpy and sympy.

My instinct says overrides (exposed via the includes and excludes methods) is a higher level feature that probably doesn't belong in beartype. It also fights with the idea of an LRU cache, because these exceptions should be permanent to be useful. Assuming that instinct is a good one, there are a couple paths forward: 1) beartype and numerary each maintain their own implementations because they have different requirements; or 2) try to tease out overrides into their own layer that would likely remain in numerary, perhaps at the expense of a (hopefully slight) performance hit in numerary.

Either way, we probably start with something that looks a lot like path 1 as a practical limitation of roll-out, so I think I have time to mull about this one without holding anything up. The more urgent question is: Does beartype want overrides at all? If it does (i.e., my instincts are wrong), that's good to know now.


¹ For the record, these are just words to me. I think I've heard them used together, but you shouldn't trust my memory. I don't understand tensors, much less their densities, and couldn't tell you whether computing tensor densities was a thing, much less how resource-intensive it was. I was just trying to sound smart.

² I fully acknowledge that my rants are exclusively my own and should remain so. Any migration of technology from numerary to beartype should come without any advocacy baggage, however (in)artful.

@leycec
Copy link
Member

leycec commented Jan 22, 2022

When brilliance refracts itself through a prism that suspiciously resembles a little-known album cover from a little-known 70's band whose name possibly rhymes with Blink Boyd, the above galaxy brain-tier post from @posita is what humanity gets. "DropBox, I summon you to pay this man in all of your corporate assets immediately!" ...fingers crossed

...I can tackle a (rant-free²) PR as a starting point.

This. So much this. Actually, I'm delighted to hack at this too – but currently preoccupied (to an unhealthy degree that our cats find disturbing yet compelling) with finalizing the upcoming beartype configuration API.

If we leave this to me, we leave this in the hands of Fate. Having read Pratchett, I trust Fate less than I used to.

...beartype's philosophy is to do what it can for various Python versions, falling back to less cool stuff where needed, but without imposing additional runtime dependencies.

This. In theory, silently integrating typing_extensions into beartype.typing without anyone noticing is on the beer-stained table. In practice, actually making that work without either mypy or beartype vomiting into its own sticky craw takes that back off the table.

Beartype: it cleans your table so you don't have to.

Option 3 - beartype.typing.Protocol is the caching version

This. beartype.typing.Protocol solves everything so nobody ever has to suffer under the heavy load of typing.Protocol again.

Beartype: it takes the load right off you and puts it right on itself, because that's what caring and sharing means. ❤️‍🔥

My instinct says overrides (exposed via the includes and excludes methods) is a higher level feature that probably doesn't belong in beartype.

This. I'm fuming that complex dribbles all over the isinstance() builtin like a raving raccoon trash-digging through our mid-summer compost pile, but we'll probably have to pretend that never happened.

The overriding constraint for beartype.typing is that it preserve a one-to-one API mapping with typing. Thankfully, numerary exists to violate that mapping and fix all the mistakes of the inglorious past.

numerary: where I will drive anyone who complains about numeric type-checking to.

beartype and numerary` each maintain their own implementations because they have different requirements...

This. I briefly contemplated refactoring numerary to subclass its own caching protocol implementation from beartype.typing.Protocol. Then I remembered the last five minutes of Pi as the refrain of a little-known 90's song rebounded through my earwax-laden canals: "Gotta keep 'em separated."

I was just trying to sound smart.

This. So much this. You will be both pleased and horrified to learn that my wife insists (despite lingering doubts I share) that computing tensor densities on microcontrollers is now a thing that exists, because NumPy has been ported to MicroPython and CircuitPython as a thing called ulab.

If She suspects I doubt, it will not go well with me tonight. I now go to gird my loins in preparation.

@posita
Copy link
Collaborator Author

posita commented Jan 22, 2022

… You will be both pleased and horrified to learn that my wife insists (despite lingering doubts I share) that computing tensor densities on microcontrollers is now a thing that exists, because NumPy has been ported to MicroPython and CircuitPython as a thing called ulab.

😳 The wonders never cease. I accept Her representations without question. 😊

beartype/beartype#86 is now up, but should be considered a work in progress.

@posita
Copy link
Collaborator Author

posita commented Jan 22, 2022

[Moved from #7, since I think I finally understand the request(s).]

Discussion topic: How should we allow a cache limit to be tuned?

I know the subject of runtime configurability has come up before. This one's tricksy. The temptation is to invite and set any limit override at load time, not at any arbitrary point at runtime. (The first thing that comes to mind is an environment variable?) I suppose we could expose an interface to CachingProtocolMeta, but we'd probably have to worry about thread-safety and all that stuff? Maybe not?

UPDATE: I'm guessing this [emphasis mine]…

but currently preoccupied (to an unhealthy degree that our cats find disturbing yet compelling) with finalizing the upcoming beartype configuration API.

… might be of assistance here?

Discussion topic: Should the LRU mechanism be CacheLruStrong or something else?

Cautions aside, I'm a huge fan of not reinventing wheels. I don't think strong references are an issue. (My assumption is that Protocols are typically defined at the module scope and stay around forever, so there wouldn't be any benefit to weak-refs.) What I am a little nervous about (perhaps unnecessarily, I haven't tested it) is the locking overhead.

FIRST UPDATE: I did some quick-and-dirty before/after testing where I replaced CachingProtocolMeta's non-guarded dict with an instance of CacheLruStrong(128).

perf_test.ipy

Carried over and tweaked from beartype/beartype#67.

# perf_test.ipy  # <-- that's IPython-speak for the uninitiated
from decimal import Decimal
from fractions import Fraction

import sympy

from numerary.types import SupportsComplexOps  # CachingProtocolMeta version
from numerary.types import _SupportsComplexOps  # non-caching version

# idealized baseline
# print(f"%timeit isinstance(1, int)")
# %timeit isinstance(1, int)
# print()

one_int = 1
two_float = 2.0
three_dec = Decimal(3)
four_frac = Fraction(4)
five_sym = sympy.sympify(5)
vals = (one_int, two_float, three_dec, four_frac, five_sym)

def supports_complex(arg: _SupportsComplexOps):
  assert isinstance(arg, _SupportsComplexOps)

def supports_complex_cached(arg: SupportsComplexOps):
  assert isinstance(arg, SupportsComplexOps)

for v in vals:
  for f in (supports_complex, supports_complex_cached):
    print(f"timing {f.__name__} with {v} ({type(v)})")
    %timeit f(v)
  print()
Before:
% ipython perf_test.ipy
timing supports_complex with 1 (<class 'int'>)
9.78 µs ± 170 ns per loop (mean ± std. dev. of 7 runs, 100,000 loops each)
timing supports_complex_cached with 1 (<class 'int'>)
242 ns ± 1.59 ns per loop (mean ± std. dev. of 7 runs, 1,000,000 loops each)

timing supports_complex with 2.0 (<class 'float'>)
9.94 µs ± 300 ns per loop (mean ± std. dev. of 7 runs, 100,000 loops each)
timing supports_complex_cached with 2.0 (<class 'float'>)
241 ns ± 0.486 ns per loop (mean ± std. dev. of 7 runs, 1,000,000 loops each)

timing supports_complex with 3 (<class 'decimal.Decimal'>)
9.77 µs ± 157 ns per loop (mean ± std. dev. of 7 runs, 100,000 loops each)
timing supports_complex_cached with 3 (<class 'decimal.Decimal'>)
244 ns ± 6.07 ns per loop (mean ± std. dev. of 7 runs, 1,000,000 loops each)

timing supports_complex with 4 (<class 'fractions.Fraction'>)
9.74 µs ± 130 ns per loop (mean ± std. dev. of 7 runs, 100,000 loops each)
timing supports_complex_cached with 4 (<class 'fractions.Fraction'>)
249 ns ± 0.647 ns per loop (mean ± std. dev. of 7 runs, 1,000,000 loops each)

timing supports_complex with 5 (<class 'sympy.core.numbers.Integer'>)
9.8 µs ± 125 ns per loop (mean ± std. dev. of 7 runs, 100,000 loops each)
timing supports_complex_cached with 5 (<class 'sympy.core.numbers.Integer'>)
251 ns ± 1.32 ns per loop (mean ± std. dev. of 7 runs, 1,000,000 loops each)
After:
% ipython perf_test.ipy
timing supports_complex with 1 (<class 'int'>)
9.58 µs ± 129 ns per loop (mean ± std. dev. of 7 runs, 100,000 loops each)
timing supports_complex_cached with 1 (<class 'int'>)
878 ns ± 6.9 ns per loop (mean ± std. dev. of 7 runs, 1,000,000 loops each)

timing supports_complex with 2.0 (<class 'float'>)
9.68 µs ± 164 ns per loop (mean ± std. dev. of 7 runs, 100,000 loops each)
timing supports_complex_cached with 2.0 (<class 'float'>)
884 ns ± 17 ns per loop (mean ± std. dev. of 7 runs, 1,000,000 loops each)

timing supports_complex with 3 (<class 'decimal.Decimal'>)
9.68 µs ± 205 ns per loop (mean ± std. dev. of 7 runs, 100,000 loops each)
timing supports_complex_cached with 3 (<class 'decimal.Decimal'>)
890 ns ± 10.4 ns per loop (mean ± std. dev. of 7 runs, 1,000,000 loops each)

timing supports_complex with 4 (<class 'fractions.Fraction'>)
9.73 µs ± 156 ns per loop (mean ± std. dev. of 7 runs, 100,000 loops each)
timing supports_complex_cached with 4 (<class 'fractions.Fraction'>)
880 ns ± 16.8 ns per loop (mean ± std. dev. of 7 runs, 1,000,000 loops each)

timing supports_complex with 5 (<class 'sympy.core.numbers.Integer'>)
9.75 µs ± 155 ns per loop (mean ± std. dev. of 7 runs, 100,000 loops each)
timing supports_complex_cached with 5 (<class 'sympy.core.numbers.Integer'>)
889 ns ± 28.9 ns per loop (mean ± std. dev. of 7 runs, 1,000,000 loops each)

Let's call it around a 3× hit over my naive (admittedly non-thread safe) approach, but still a 10× improvement over not caching.

SECOND UPDATE: I removed the with self._lock: contexts from CacheLruStrong and tested again to try to tease out the LRU logic from the locking overhead.

With LRU logic, but without locks:
% ipython perf_test.ipy
timing supports_complex with 1 (<class 'int'>)
9.58 µs ± 69.2 ns per loop (mean ± std. dev. of 7 runs, 100,000 loops each)
timing supports_complex_cached with 1 (<class 'int'>)
632 ns ± 15 ns per loop (mean ± std. dev. of 7 runs, 1,000,000 loops each)

timing supports_complex with 2.0 (<class 'float'>)
9.55 µs ± 161 ns per loop (mean ± std. dev. of 7 runs, 100,000 loops each)
timing supports_complex_cached with 2.0 (<class 'float'>)
618 ns ± 2.58 ns per loop (mean ± std. dev. of 7 runs, 1,000,000 loops each)

timing supports_complex with 3 (<class 'decimal.Decimal'>)
9.56 µs ± 141 ns per loop (mean ± std. dev. of 7 runs, 100,000 loops each)
timing supports_complex_cached with 3 (<class 'decimal.Decimal'>)
623 ns ± 4.09 ns per loop (mean ± std. dev. of 7 runs, 1,000,000 loops each)

timing supports_complex with 4 (<class 'fractions.Fraction'>)
9.55 µs ± 150 ns per loop (mean ± std. dev. of 7 runs, 100,000 loops each)
timing supports_complex_cached with 4 (<class 'fractions.Fraction'>)
622 ns ± 7.43 ns per loop (mean ± std. dev. of 7 runs, 1,000,000 loops each)

timing supports_complex with 5 (<class 'sympy.core.numbers.Integer'>)
9.58 µs ± 126 ns per loop (mean ± std. dev. of 7 runs, 100,000 loops each)
timing supports_complex_cached with 5 (<class 'sympy.core.numbers.Integer'>)
629 ns ± 8.22 ns per loop (mean ± std. dev. of 7 runs, 1,000,000 loops each)

Assuming my methodology was sound, very roughly 250 ns is my naive base implementation, 250 ms is locks and 350-400 ns is LRU accounting.

THIRD UPDATE (mostly tangential): After some casual reading, I don't think this is a strong case to revive beartype/beartype#31 (i.e., that we could use this exercise as an excuse to implement LRUDuffleCacheStrong). As far as I understand those motivations, they are targeted at keeping the aggregate size of cached entries within a boundary (vs. number of entries). We're caching bools and references to types here, number of entries should closely correlate with aggregate size. I might have misunderstood, though.

FOURTH UPDATE: Food for thought on thread safe access of dicts….

FIFTH UPDATE TANGENT: By the way, for those playing our home game, this is an awesome public service announcement (with guitars).

Meta-discussion topic: Is a per-Protocol cache even the right approach? Should we consider a global one instead?

Under numerary's current implementation, each Protocol maintains a separate cache of what types do and do not "comply" with its interfaces. I haven't thought this through, but I could see a case for creating a single global cache instead. I think it makes tunability and profiling much more important (especially if one is using the mechanism and also relying on a third party library that uses the same mechanism under the covers). My instinct is to keep the Protocol-scoped cache, but I wanted to throw this out there.


☝️ Happy to have those discussions here, in beartype/beartype#86, or some other place that you prefer.

@leycec
Copy link
Member

leycec commented Jan 26, 2022

Amazing discussion continues amazing.

A Configuration API Arises from the Blackest Depths of Moria

Firstly, the public gala unveiling the beartype configuration API is happening now and you're the only one invited: squeeeee

from beartype import beartype, BeartypeConf, BeartypeStrategy

# Configure @beartype to do all these things:
# * Type-check everything in O(n) linear time.
#   This is currently ignored. So, we're still gonna
#   type-check a single thing in O(1) constant time.
#   This is the cost of personal sloth and pizza.
# * Print the signature and body of the
#   type-checking wrapper function dynamically
#   generated by @beartype here to stdout.
@beartype(conf=BeartypeConf(
    strategy=BeartypeStrategy.On,
    is_print_wrapper_code=True,
))
def public_service_announcement(with_guitars: list[str]) -> None:
    print('These are your rights:\n' + '\n'.join(with_guitars))

@beartype: freedom may still not be free, but at least we can type-check it.

Given beartype.BeartypeConf, we absolutely can, should, and must stuff everything (including that leaky kitchen sink we haven't cleaned in five years according to an annoying calendar app) that is configurable into that class. See the super-secret beartype._decor.conf submodule that has a license to kill... bugs, that is. 🥁

But first...

Safe Threads Make Safe Sweaters

Secondly, thread-safety.

The 💡 realization I've had over the past several lifetimes as a poorly paid techbro is that thread safety only really matters when the consequences of violating thread-safety are both harmful and permanent – like collapsing into a snow drift five yards from our cabin's front door without double mittens on, which is not gonna end well. Thankfully, that's basically never the case for memoization, caching, or other optimizations.

For caching, violating thread-safety usually just means that contesting threads repeatedly cache the same thing as they trip over each trying to get out of a snow drift five yards from our cabin's front door without double mittens on. That's usually fine. Is that fine here? I know nothing, but kinda suspect we're better off without lugging around a giant deadweight self._lock everywhere.

Less Work Means Less Migraines

My instinct is to keep the Protocol-scoped cache...

Thirdly, my instinct is to keep following your instinct. Instinctual recursion: engage!

We do as you say in all things. Lastly...

How Much Space Are We Talkin' Here?

Since...

We're caching bools and references to types here...

...we may not even need to deep-dive into LRU caching here. I know, right? I'm squeeing again over here. Specifically:

  • bool is int, and Python interns (i.e., reduces to singletons) all integers in the range [0, 255]; since booleans are integers in the range [0, 1], Python thus interns all booleans. Basically no space consumption there.
  • Likewise, classes are typically module-scoped (exactly as you say) and thus preserved in memory for the process lifetime. Yet again, basically no space consumption there either.

true hero, bro
What fearsomely heroic magic is this!?!?

My festering guts are insinuating that LRU caching might be overkill here. What say you before faceplanting into the nearest cushion with unspeakable exhaustion, Typing Overlord Posita?

@posita
Copy link
Collaborator Author

posita commented Apr 2, 2022

beartype/beartype#117 aside (which I hope I didn't cause), I think we can call this fixed by beartype/beartype#86 and beartype/beartype#103?

@posita posita closed this as completed Apr 2, 2022
@leycec
Copy link
Member

leycec commented Apr 5, 2022

Jolly good show, bear bro! I meant to close this out for you a few weeks ago, but then Russia repeatedly blew up Ukraine and all I ended up doing was wearing out my <F5> key on /r/Ukraine.

Thanks again for all that fast-actin' protocol cachin'. I'm currently hip-deep wading through the uncomposted manure that is Sphinx configuration. This is why I've been quiet on the @beartype and numerary front. Sphinx is the pain that assaults my brain.

Please, please, please drop me a line at any time if numerary has any further needs or invents any further piles of code gold that we can run away with and pretend were ours all along. 😏

@posita
Copy link
Collaborator Author

posita commented Apr 5, 2022

Sphinx. Yes. Ugh. Your observations are apt. Getting Sphinx to work is like "implementing" SAP (which is one of those rare cases where I think "implement" is the right word to describe a software installation¹). I used to fight with it on all my projects, including well after the getting-it-up-and-running phase. RtD was cool, but finicky. (I hope that's changed.) Sphinx and rST is precisely the reason I now use MkDocs (with Material) and Markdown. (No more RtD integration. I now publish straight to GitHub pages for each release, although I haven't yet invested the time to automate this.) It hasn't been without issue, and I gave up a lot of shiny promises with Sphinx², but I didn't use those anyway, and I'm much happier.

I would offer to assist, but I've long since forgotten (or willfully suppressed) any memory around my troubles with Sphinx. You're welcome to ask me for help at any time. I just can't guarantee I'll be useful. Heck, I can't guarantee I won't be involuntarily triggered into a weeping mess to be scooped up off the floor with a spatula. But I'm willing to give it a shot.

¹ Hint: if I need to "implement" your software in order to use it, you don't have not delivered a product. You have delivered a liability. But, hey, what do I know?

² I mean who hasn't dreamed of a single documenting system that supports multi-language cross-references?

@posita
Copy link
Collaborator Author

posita commented Apr 5, 2022

Oh! One thing that did occur to me is that caching protocols may have value orthogonal to beartype. This may make runtime beartype configuration (which is a subject I know you've given some thought) more useful. Consider, for example, the case where one wants to start retrofitting the @beartype decorator to a large codebase and to pick up the benefits of caching protocols. A desirable approach might be to start using the decorator incrementally in CI, but not in production while using caching protocols in all environments. It's not immediately straightforward how one would accomplish this. numerary inartfully walks that line by requiring an environment variable to be set in order to "activate" the @beartype decorator in its own codebase. There are probably better approaches.

@leycec
Copy link
Member

leycec commented Apr 6, 2022

SAP

Enterprise JavaBeans PTSD intensifies. It's best for both of us that we not relive those hateful memories.

...if I need to "implement" your software in order to use it, you don't have not delivered a product. You have delivered a liability.

Quoted for truth. Animated for feels.

this is me

...MkDocs (with Material) and Markdown...

Keep selling me on this. I'm an easy customer here, because my black heart is already filled with vengeance at the vendor I'm locked in with what feels suspiciously like a straightjacket.

...publish straight to GitHub pages...

Sold. Static site generators is my jam – or would be, if I webdeved. Which I totally could. You know. Like... any minute. <crickets_chirp/>

Sadly, I'm reliving 90's flashbacks of "I'm a Teenage Dirtbag." So, not teenage but a dirtbag? Long Grecian tragedy story short (that could very well end with someone depravedly clawing out their own sensory apparatus): @beartype has so much content locked up in reStructuredText – including a literal dumpster's worth of embedded docstrings and a README.rst that frightens even Pandoc (and nothing frightens Pandoc) – that abandoning Sphinx for verdant fields elsewhere is probably a non-starter.

I value my sanity, so I wish that abandoning Sphinx was a starter. MkDocs + Material + GitHub Pages: take me now!

The only winning move is not to play. We all remember hearing that somewhere. Now, we are living that moment.

A desirable approach might be to start using the decorator incrementally in CI, but not in production...

Bwaha! Code or it didn't happen, so:

from beartype import (
    BeartypeConf, BeartypeStrategy, beartype)

beartype_conf = BeartypeConf(
    strategy=(
        BeartypeStrategy.O0  # <-- no-time is a thing
        if MUH_BURNING_PRODUCTION_LINE else
        BeartypeStrategy.O1  # <-- linear-time for justice
    )
)
'''
Beartype configuration suitable for the current environment.
Specifically:

* On production, this configuration currently disables the
  :func:`beartype.beartype` decorator entirely.
* On CI, this configuration enables the default ``O(1)``-time
  :func:`beartype.beartype` decorator behaviour.
'''

@beartype(conf=beartype_conf)
def make_it_so_number_one() -> str:
    return 'My beard despises you, Jean Luc.'

Relatedly, German Megamind @amogorkon recently invented this phenomenal Python swag called justuse that lets you violently decorate all callables defined by any arbitrary module with @beartype at any arbitrary time:

import beartype, numpy, use

# Type-check the entire NumPy API with @beartype.
use.apply_aspect(numpy, beartype)

# Type-check @beartype itself with @beartype.
use.apply_aspect(beartype, beartype)

# Type-check your production line with @beartype
# using the aforementioned configuration. Hazzuh!
import burn_it_down
use.apply_aspect(burn_it_down, beartype(conf=beartype_conf))

Apparently, Rome was built in a day. 🏟️

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 a pull request may close this issue.

2 participants