From 4234bf8ce319bf8c2db8cc8d001618b70c29b1b0 Mon Sep 17 00:00:00 2001 From: jab Date: Mon, 26 Feb 2018 23:29:56 +0000 Subject: [PATCH] split back out (Ordered)BidictBase classes, improve namedbidict validation, docs --- CHANGELOG.rst | 82 ++++++++++++------------ CONTRIBUTING.rst | 4 +- README.rst | 6 +- _static/type-hierarchy.png | Bin 10646 -> 27108 bytes _static/type-hierarchy.txt | 20 +++--- bidict/__init__.py | 6 +- bidict/_bidict.py | 13 ++-- bidict/_frozen.py | 28 +++++---- bidict/_named.py | 32 ++++++---- bidict/_ordered.py | 38 ++++++----- docs/addendum.rst | 2 +- docs/api.rst | 2 +- docs/frozenbidict.rst.inc | 7 ++- docs/learning-from-bidict.rst | 9 +-- docs/orderedbidict.rst.inc | 8 +-- docs/other-bidict-types.rst | 34 ++++------ docs/polymorphism.rst.inc | 115 +++++++++++++++++++++++----------- tests/test_hypothesis.py | 17 +++-- 18 files changed, 250 insertions(+), 173 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 3168eb8..1e75f78 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -6,18 +6,6 @@ Changelog .. include:: release-notifications.rst.inc -Type Hierarchy Reminder ------------------------ - -When reading the below, -remember that :class:`bidict.bidict` extends :class:`bidict.frozenbidict`, and -:class:`~bidict.OrderedBidict` extends :class:`~bidict.FrozenOrderedBidict`. -So the changes to the frozen bidict types described below -often apply to the non-frozen types as well. - -See also :ref:`bidict-type-hierarchy`. - - 0.15.0 (not yet released) ------------------------- @@ -39,13 +27,13 @@ Speedups and memory usage improvements See the new :ref:`inv-avoids-reference-cycles` documentation. Fixes `#24 `_. -- Make :func:`bidict.frozenbidict.__eq__` significantly +- Make :func:`bidict.BidictBase.__eq__` significantly more speed- and memory-efficient when comparing to a non-:class:`dict` :class:`~collections.abc.Mapping`. (``Mapping.__eq__()``\'s inefficient implementation will now never be used.) The implementation is now more reusable as well. -- Make :func:`bidict.FrozenOrderedBidict.__iter__` as well as +- Make :func:`bidict.OrderedBidictBase.__iter__` as well as equality comparison slightly faster for ordered bidicts. Minor Bugfix @@ -59,7 +47,7 @@ Minor Bugfix (with ``_fwdm_cls`` and ``_invm_cls`` swapped) is now correctly computed and used automatically for your custom bidict's - :attr:`~bidict.frozenbidict.inv` bidict. + :attr:`~bidict.BidictBase.inv` bidict. Miscellaneous +++++++++++++ @@ -73,9 +61,9 @@ Miscellaneous it now ensures it is :func:`callable` before returning the result of calling it. -- :func:`~bidict.frozenbidict.__repr__` no longer checks for a ``__reversed__`` +- :func:`~bidict.BidictBase.__repr__` no longer checks for a ``__reversed__`` method to determine whether to use an ordered or unordered-style repr. - It now calls the new :func:`~bidict.frozenbidict.__repr_delegate__` instead + It now calls the new :func:`~bidict.BidictBase.__repr_delegate__` instead (which may be overridden if needed), for better composability. Minor Breaking API Changes @@ -83,13 +71,21 @@ Minor Breaking API Changes The following breaking changes are expected to affect few if any users. +- Split back out the :class:`~bidict.BidictBase` class + from :class:`~bidict.frozenbidict` + and :class:`~bidict.OrderedBidictBase` + from :class:`~bidict.FrozenOrderedBidict`, + reverting the merging of these in 0.14.0. + Having e.g. ``issubclass(bidict, frozenbidict) == True`` was confusing, + so this change makes that no longer the case. + - Rename: - - ``bidict.frozenbidict.fwdm`` → ``._fwdm`` - - ``bidict.frozenbidict.invm`` → ``._invm`` - - ``bidict.frozenbidict.fwd_cls`` → ``._fwdm_cls`` - - ``bidict.frozenbidict.inv_cls`` → ``._invm_cls`` - - ``bidict.frozenbidict.isinv`` → ``._isinv`` + - ``bidict.BidictBase.fwdm`` → ``._fwdm`` + - ``bidict.BidictBase.invm`` → ``._invm`` + - ``bidict.BidictBase.fwd_cls`` → ``._fwdm_cls`` + - ``bidict.BidictBase.inv_cls`` → ``._invm_cls`` + - ``bidict.BidictBase.isinv`` → ``._isinv`` Though overriding ``_fwdm_cls`` and ``_invm_cls`` remains supported (see :ref:`extending`), @@ -108,7 +104,8 @@ The following breaking changes are expected to affect few if any users. ``DuplicationPolicy.RAISE.RAISE.RAISE...`` - :func:`~bidict.namedbidict` now raises :class:`TypeError` if the provided - ``base_type`` is not a subclass of :class:`~bidict.frozenbidict`. + ``base_type`` is not a :class:`~bidict.BidirectionalMapping` + with the required attributes. - Pickling ordered bidicts now requires at least version 2 of the pickle protocol. @@ -148,8 +145,8 @@ The following breaking changes are expected to affect few if any users. (e.g. ``f = frozenbidict(); {f.inv: '...'}``) would cause an ``AttributeError``. -- Fix a bug introduced in 0.14.0 for Python 2 users where calling - :meth:`~bidict.frozenbidict.viewitems` +- Fix a bug introduced in 0.14.0 for Python 2 users + where attempting to call ``viewitems`` would cause a ``TypeError``. Thanks Richard Sanger for `reporting `_. @@ -178,7 +175,7 @@ The following breaking changes are expected to affect few if any users. frozen bidict and some other immutable mapping that it compared equal to into the same set or mapping. -- Add :meth:`~bidict.FrozenOrderedBidict.equals_order_sensitive`. +- Add :meth:`~bidict.OrderedBidictBase.equals_order_sensitive`. - Reduce the memory usage of ordered bidicts. @@ -221,11 +218,11 @@ This release includes multiple API simplifications and improvements. together and remove ``BidictBase``. :class:`~bidict.frozenbidict` is now the concrete base class that all other bidict types derive from. - See the updated :ref:`bidict-type-hierarchy`. + See the updated :ref:`bidict-types-diagram`. - Merge :class:`~bidict.frozenbidict` and ``FrozenBidictBase`` together and remove ``FrozenBidictBase``. - See the updated :ref:`bidict-type-hierarchy`. + See the updated :ref:`bidicts-type-diagram`. - Merge ``frozenorderedbidict`` and ``OrderedBidictBase`` together into a single :class:`~bidict.FrozenOrderedBidict` @@ -233,18 +230,18 @@ This release includes multiple API simplifications and improvements. :class:`~bidict.OrderedBidict` now extends :class:`~bidict.FrozenOrderedBidict` to add mutable behavior. - See the updated :ref:`bidict-type-hierarchy`. + See the updated :ref:`bidicts-type-diagram`. -- Make :meth:`~bidict.FrozenOrderedBidict.__eq__` +- Make :meth:`~bidict.OrderedBidictBase.__eq__` always perform an order-insensitive equality test, even if the other mapping is ordered. Previously, - :meth:`~bidict.FrozenOrderedBidict.__eq__` + :meth:`~bidict.OrderedBidictBase.__eq__` was only order-sensitive for other ``OrderedBidictBase`` subclasses, and order-insensitive otherwise. - Use the new :meth:`~bidict.FrozenOrderedBidict.equals_order_sensitive` + Use the new :meth:`~bidict.OrderedBidictBase.equals_order_sensitive` method for order-sensitive equality comparison. - ``orderedbidict._should_compare_order_sensitive()`` has been removed. @@ -280,11 +277,11 @@ This release includes multiple API simplifications and improvements. - Rename: - - ``bidict.BidictBase._fwd_class`` → ``bidict.frozenbidict.fwd_cls`` - - ``bidict.BidictBase._inv_class`` → ``bidict.frozenbidict.inv_cls`` - - ``bidict.BidictBase._on_dup_key`` → :attr:`bidict.frozenbidict.on_dup_key` - - ``bidict.BidictBase._on_dup_val`` → :attr:`bidict.frozenbidict.on_dup_val` - - ``bidict.BidictBase._on_dup_kv`` → :attr:`bidict.frozenbidict.on_dup_kv` + - ``bidict.BidictBase._fwd_class`` → ``.fwd_cls`` + - ``bidict.BidictBase._inv_class`` → ``.inv_cls`` + - ``bidict.BidictBase._on_dup_key`` → :attr:`~bidict.BidictBase.on_dup_key` + - ``bidict.BidictBase._on_dup_val`` → :attr:`~bidict.BidictBase.on_dup_val` + - ``bidict.BidictBase._on_dup_kv`` → :attr:`~bidict.BidictBase.on_dup_kv` 0.13.1 (2017-03-15) @@ -350,8 +347,7 @@ This release includes multiple API simplifications and improvements. or suggestions for an alternative implementation, please `share your feedback `_. -- Add :attr:`_fwd_class ` and - :attr:`_inv_class ` attributes +- Add ``_fwd_class`` and ``_inv_class`` attributes representing the backing :class:`~collections.abc.Mapping` types used internally to store the forward and inverse dictionaries, respectively. @@ -448,9 +444,9 @@ This release includes multiple API simplifications and improvements. - More efficient implementations of :func:`~bidict.util.pairs`, :func:`~bidict.util.inverted`, and - :func:`~bidict.frozenbidict.copy`. + :func:`~bidict.BidictBase.copy`. -- Implement :func:`~bidict.frozenbidict.__copy__` +- Implement :func:`~bidict.BidictBase.__copy__` for use with the :mod:`copy` module. - Fix issue preventing a client class from inheriting from ``loosebidict`` @@ -539,7 +535,7 @@ Breaking API Changes ++++++++++++++++++++ - Remove ``bidict.__invert__``, and with it, support for the ``~b`` syntax. - Use :attr:`~bidict.frozenbidict.inv` instead. + Use :attr:`~bidict.BidictBase.inv` instead. `#19 `_ - Remove support for the slice syntax. @@ -547,7 +543,7 @@ Breaking API Changes `#19 `_ - Remove ``bidict.invert``. - Use :attr:`~bidict.frozenbidict.inv` + Use :attr:`~bidict.BidictBase.inv` rather than inverting a bidict in place. `#20 `_ diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 367b7d8..8c93ec2 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -132,8 +132,8 @@ Besides filing issues and pull requests, there are other ways to contribute. - If bidict has helped you accomplish your work, especially work you've been paid for, - please support bidict's continued maintenance and development - financially, and/or ask your organization to do the same: + please consider supporting bidict's continued maintenance and development + financially if possible, and/or ask your organization to do the same: .. image:: ./_static/support-on-gumroad.png :target: https://gumroad.com/l/bidict diff --git a/README.rst b/README.rst index c5eb5fc..d0884ed 100644 --- a/README.rst +++ b/README.rst @@ -122,8 +122,6 @@ Notice of Usage If you use bidict, and especially if your usage or your organization is significant in some way, please let me know. -Hearing that people are using bidict is a powerful antidote -to the loneliness of maintaining an open-source project by myself. 😅 You can: @@ -204,13 +202,13 @@ to help with your review, I would be happy to try to coordinate a screenshare. -Becoming a sponsor +Becoming a Sponsor ^^^^^^^^^^^^^^^^^^ If bidict has helped you accomplish your work, especially work you've been paid for, please consider supporting bidict's continued maintenance and development -and/or asking your organization to do the same. +financially if possible, and/or ask your organization to do the same. .. image:: https://raw.githubusercontent.com/jab/bidict/master/_static/support-on-gumroad.png :target: https://gumroad.com/l/bidict diff --git a/_static/type-hierarchy.png b/_static/type-hierarchy.png index 001c42e68da951e26cc9bfe992805858e1a08cfc..ea4cfcf48c99a3ed154fd240352d3d70405e5f1a 100644 GIT binary patch literal 27108 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 z

G#kL6xndXq-UxK81b$1hUg1lDH^<&$_YS-B@sR=i*a9Oj8=`Q?{IeMeq$}U6{ z(F-_G0nKCCTYm$i%t|Q;k1`!fb)nPL3&SZ@Ea@lKWyJv=@mky+w*;62J^6V}o}_s2 z%+-VDHtwo0-eag4sE2zwvs1}kqv(x=_To+YoxVgY(lBCfGITw^0ms$%Ir#a6`g=+$ zib1UlN~309W$l3U!AFPY5wI$K%uOnFP$Wk{M@6Cu7kMcTTSyr);vkZbrgLyj-l{`In+AA3wI!9Hb z(ps20h1ah%KIiEMtk9^rsvLVrlY8iwAy?9q#%s7^dERJ#9b2S?V3cOqdt)1$V}TQV zDfpW57c2~y+YGr4*w!U=ya3)8NWc6TkH}QchuvhPmiH%G+gma>ecP*^Zr%Qjaf{(W zrh3_KFbk#%GFi#xYv)>Sf57~jJ}m?mrBId9%N+R`Uy`k7^CzK+o$gl8qT9hwzporq z87GqI=k7ib-@1YU^ZV8C#*b3yT^J2|ptEA_%1xvbEGi@IT7-{D@EZu7n0v&G-7lH= zqh!+Cnf{|(7(D8G`jC`^yHCj>?n|g$#;jv&33wrj(5G+1(kWw0N+(xi>Mc@WpDv#J zguQWY>Xw>t5Jbo{po(B$l-8Q6-vU;s_N7m=wOWMyF6Q`+-7Xb7rY_wWqMAh7zII5n zu(MAfS>k(@>ci+n{ksC&qB*|%R|`rEi;Be{2;h%V_Bw}31!Cy8qc`(weZD<|lWzmr3Z<+S_m|p2c z(WHH2sQn{5nXm7qSefF}#0~47THBSqmojfIH*S5kCqCQ7I@Wn`pYdQj`^g72s+UsT z!fJ?)*6598uM@FC6!VP_A;+Ncd#jFZJ7t5>NAl>G4QU*wY2Ob*#&5Vs@x8@IO-P;a zhm2Nr*LGw(+V_B>=(CT|(dL=$>S5HrNW_O#MRBIy$c(j1U+^VGIJ<+p_{KK2-1!F= z{|k6T?3mvUmoYURwVIX@QZg^fA-RqL1t|?iRIrOeWbsT-h7Tud1A(r;{9G78oiFz4 z=BHG*NlSJ#FYO`?w$+E6cVc5|;+kyv8D-()%tjyyn@^lyR7>Jx=KXcXtlf3l1PRW2rd6;jD961eh@a-Re=9l29{C&hVIHS;~& z_GrJA3o}m9-XPkFcT{w7KIIQ1VQed3=xeSG4&o?!O^15sAYMmeNTztKkyF%1aIfx# zZ6l3Qpx5#qKI4)(dot-=s9;G#(c7O}hS2D4&KQ(*oQ~_nQ*W7pW%D5O`3 zcJ@CGwkySW@tM^)Fu%CiYF~42S2`?UHaoFyXsH$d6EQE_-A?Cke*uo*-U864u`Y?8 z5Vb#Eaa1{0GYkI`mwAL&C{Ne;Csb;4V(NAV_1ZQxD|In8Y7y|r8+1=cYvot{+l+Ve zUhes{KI(8{qjngJKA%!J z=^9CI^gtJyAtYetZNts$=bEI&aM@Vwy+O+`<=a`flhHR+X@qw@)cL8WK0gs}Dg}*f z28B$6rj+Ozw5@&HZ9$Z4AjPnZqx~E8>byyF9=o(%5Vo=p}?NCNue5-TPoa_^gc&u55A}?hZKIZ$qg~duUIl zr#G+qlkhTdIqxXNg-IXth1|Js@NPW-F!dS%dTLPp8d|S($#si_ZyApe)I3Bt z5eeZ)2HLfcTDE0~teKkpF!b336QFPx_$%t?qvHUn4a?&x7Fj$xkP-KR%GKDnxnucm zMP5Y5YDr95CqbT4yzSEpxc%&lx`52pt88yK12OBlADew*0IjY4Hp27SuN*8}Z20sQ zhr**>rQ|?&G*l)2YW-qf}JA(Y$H8frlSo)r*95B*Y${c%bKMlGdI9`E$ zcXt=?_q)e`fBu_oR*a7}OeBI~FT08_$v>`?{6K+3`Ztk{CGN4;#OK4JN;yFP;#&~o zZyFw4{4ZbMWzu#J(;h`{2R~NR?kV}r>Hc3Ghd)ocFy1pu8)WU_sa;97QUXQppOyf= zq;~%VpiHf&BLDE-SScJAPHK;Kg|#;5$=>`}{+kV7o^}mNgxLXAF?N+WhoE>)*`BFY z1iQ0j!knL@?P_@s0WYDh4G4NRjQmUCw<8?lv|#CC;QHOI(KmJvA6kuKUjvp`Pc6l2 zr~zx59f2}Nz65gs2iT?tc2ob-?=hADqLs&JgI zUE1yq#MEX0Sp4}Q_$Z8*5(X!(0$q(H?1L3NF{D0NwHQ`jkk(oc+Sb1%Ki3rgv^(Ik zfXl}=55A(XjbgUql05$Y9p>+^9~45DOojp*j5-T39{PaV+RB=hv*&v5)EtRU|kjb$NH?P2&USk4Ye2`WxN)9*jy zPfh_-ZpW_Hy)(SExJ{ zx0$Xg#yW`QAy%g?H0P}jyqfF%{RGeho8&3OYK3rH^@Xj*3evPeUmNtCdKeL7jlRBn z^8qC5)ty6=K->p8LU;yiD z*6fcfSXLq&IQLmLtQT-NcH5x*1nQ5qA|pGXJuETIECl`X;&@_HKF1~b;RYYC;TjjK z+GMZA2%*WA{E1lks&%aF05m>Lfo{W`O*P$`Vw6E?`6JOdR;}41Yq}GaPcae!6a$IC zVxxd3rv`*1UqvJ_V1==h3oAgfAOyr-LfC?&8W2V{Y5H~U4#oN#VA^4`R1tg3K38a* zMuWD=OdV)rSLlG=u{y2i5s*(NVigp#B+L3E5jo^M$uA;7+uS!8fo=MlY%ykc_t`f10olr5h%4@gn-}y+U zHvsL=loiR@CJ;gnrh2Y_cZF2|_D^bzsz>@}zRp=g2EU;lqfhEo*sn1T-_uTPGbvr% zE57p}VF^uh4R5t?WP^7dO#*~DQw>0=KUTG_ETV&L$qqj|27(fvP4NM_);9(FkD#$$ zM>KLJKu;=rujSQT;nC_SI^}`oixL3+Ck@)D9|t5;=D0>Z{@@@L`QN~I;OO! zNP1M|%7+wt#+Ue0u-fJy7+{yU#5gkiFENOKKp)r=mcTQx#_ zF~yhKSu(#CHVV(dZx(|>XTsr9Ly5#`4|Xd=_*``tS#AcHa4E|@&-+ZM_7X3_Rqv*zofDZROVYf|?q>B^ z5>(&ir^d>cD2bBUQj;AJJ|>%^Nae}zZvxRkb43(&jQl&$<`^RAwBJgvmvKLscG=~P z#R=&w65!vY{AZmx+*TNH*&V#X?<46cG`kk6IQOErBwu! zd>!xE91C*giJ>Lnzzjd(qsz}QQ42*K=_~+hiDXpqbi*W)7>J)SV0P#2jaO^NZKCRi zF3irb#q9_bl_vTEJ$I2@?XOg(Cz{ClG=yy)g{MHRoYF{EJ zd6WTqYi<+uMBWu+0q~W>1|Rg~W(uUOA3Jz4@2T5q%l7z~CTOtsEJP291IOWY<_VQp zzhyK05m$0`66y$4!-7g8Iw>a8zCbYbpo_;$etIQ8VYb4vlQC#%>G;W&09tA7JY#ei zkl83eiDxnyqo4w&{MfEsrq{w8cBqB!S6;I+3!oYWlR(GoM)J$(c~-6FJLNL2 ztY(8w9X5CDhoYjg0_rH+FXSqcXU-clZd#tl zjEo3O7^=19_2 z#EuBf-HA7=X*Z)SKh!I1R+}=-i@l2Dl)E<{2vnFEeFMSl%rr$c4R8tcIMQ<9)00V6 z!SSBXx5r?9%A}2MA%RqrzZ)ojzTrw~Jc6c^PV1yu#O46nCdV!MaNbNb*q7?6&Y6J{ z3x=k+gKiuvJUKKqxuO(ru6N~daY!Ph==1bWL(D;J;9D&rPPdi2*x0NRPc~?VXdeU} zwj3OV;}Da_+%8oS?L?tU{rog_o|%UCpkBqH?cKrwcj$0npC1&3e11H+C-3zj$Ey0e z8ZI8i>8;avaJ2dx`t?NWc(^6h^zyvv9a_nndtJ-a&Z*RtX;hQ=SxpIFuvkAa*zJ|y zjalce36V;QE{_W#T@GL{1F8fl7~t!h6b9=f-(54`0&!%e+7%P(4A9gpnlogaJcwYK zV6N6Fkdl%6x+FBB00fbtcxlxe$7{}Yxx0Do*~5O{Lz=QIwgbFcU;W$f&@oj-HAFfcYO6Zh?fEbWh zyM_mgBo!ln@hFNV=^PeV=9p8bIO+mG!{irBM;a*|U0N3fC7rZd0E3(=bULj)N7Upd z96vP-tVkg`W5KB5IN#7uQT`({PnqXG7zqr)tFPErvvog72huGmwAD^djYRbAE8Hrp z{M0)3Pwa`lrFpJqZ_={rD*H1SJE9u-ct8`PC zy`#TS>QvE~|3vU9v9i2kcp#?D_@ygV%<;-Uu{P@zrvArat{)y=tEjE%Fo{K#=(N`c z&4^ZbM6Ct8c*_e)d$sgzpXSMT;3JY2NF8A%Gfc|aDrx@dTx9Pmx@JwK+Sibvr~Em` zsNai7G3&Ng^xfb(M?$|$h3`5vh-7}w*MVW4|1Bg*371hc-+ZJaH?|o`<)9d6FB8xa$40f-g zh>yOIJ&X$|zVCzP^7Z&0par?GCzi{TFfUB9x@;VMHCkGC*Ym8#4VgY{3PrI_f7mt9&&X(G|7RNL zj_AZF9Ki@BKKsjVXN!c}__DgjvrxA$ECB2Vc(b;NL+#zi-Hz z7s~BNU`3Wm8nZA7r_|Ch6z|I0-1R+#RdIV@e1RxM+i4E(8|YEYh6>l2+#XeVgODKe z!h6_C&qU03lP7=&|IM)%`G;dlDX3+se`FKeKe8339?Xn{tG(52S0X<%I4D-N{9^VC z`dKeD)_)$VNw-~v19P7^Qa4RX11*H2O@22<1L` z=AZ4yzcS4=+oM+j9&Tza?uc*#GW7H`;n-uuq*dX;PszZ5`(jXLrWZG9%~tEK0AN&9 zm6CoUObt=1l;THC4|E!?itea01_`HpQX=uCjC=VO` zyRj&hxn}KGwlN){UV@L^=`8yeX~q++$YUxvy21p&S!;xhkowYb1+$W{OM15bJLo(^}uQs}2b z-G5o2^Jx2{JWM3<{`^|g;^Ui|1>2UL%M}5g21XPF0i;q;r=;7PJG&{`GUBe zl)A?io7c+~p9YW`7GLIT^LSzFz(GmK2gLduLu!|~pX<6K0!J@*n!F=|mi=@N@cJf3 z3(%Ap2T*PxRbHs>$hTOk|8drKRSScK(W(7?pQw=VDFg%`X3TP}dll!PC?Dqp2Ofc4l08 z;VK&_{>-F~TdN@T;N^mYe2~Co6_^YY84n>??q{0JQTr0(`SLnU`?H;>yZDaa0T8L4;>)jbv22w3J(XHA$_MpG< zhVYurgK1=ET@RgGqy^dcv;b1erg*|JK~jyi58=UI(q3WMJ_ogJW97r%cxywdW?W^D zDkuX}VvF0}`79v}qJM5M-T-Hk$hj(7b3%Upam|K2gLR4cJs+l`?`ikBq?S;EA6VF( zkqMoU#3c$kn(K9MyZSdnu93qZcEoiP+i7CMud`kTCc_C9dZ5j@s^IT;EhKJRjD#FR`jiLh+tmaW zPj*X3-KV<5#0UP2h0($Fu%>g175Z)W%aO@O|2+KY<$^zDlF=YK8rt^gR23#By8}~u z6Wz`&Ry5(5UIxvO7ahN%x>?V0>vpn|dD}VX3vq$250`pvQ9sCD&}=;l+U5!s#6Jbh zm(7bSht2k+l@uN$?Br_mhIBwGe!WEb@#EO?IDQwopC5!7X3t!qv0(NmcLj(DEB)zt z|ADil96mAUf;utXmGP9SqW8cHer=#?4TzR}c5!X5s0pVY7>UyzDoX{NuRcbs*L>{C zY)H4cB>`Z3ajEOhFAG^m16eZFOE{#em%pb>%rTUZ#AFpwGu|>8A_^>eQ0KRKZBa+` z`YhX-*yk&Se6&+t7-7eyPNr{K)Paoqhd>HizTC$rnA+F*S@}V5;{3Ea+c!KPSc~x8 z$8?{2{mlCyiqW%0`bo@hbo?nJ6Ag%1L)4iv9$E~1n&07twv}$lzTqFr7%0U6F}ys} z*GzMAmY8Ys-Cdq`~~p zTj{`^#|~GMIOtHxF^NHkA=K{#h!hapkBJt1T)K>~RLGHN_F^;{li=pTSK`oKKkF6~ zx~vSAQ@=g%2PWECH`9)aQQZx14oTNLBYT;qwIQGoWx=l_cyND<`Dr8e~1`6DRvI(b(w{sx?gE9Ea^GZS!Uy+j&#h*eZy8}nXAwHMOd{7 z&E=!7`0t5K%*l->b?O9HFf>PXUKc+1JNs!{Yy*Z*i{xY&h-6(!w|;wPXj3Aev319H zVt5;&p`ZH=`IY7?v80b}OjJmskHBQa#K)(aF;PI!Jii1B5o;BJa}OB~GWnbUJ6b=L z6hpGSWgH}AG(s@Z zYgO^Repe)J>$32Z#PGOmjt_>qr8A>#TUgHwGU&JB+HS`*-_MF!lAS&6l`%g7h3kzS zK*Vy`*xqV-WUKUa!d%9k=9&}ln+he|Sd(bEXGefpoapjmiKgaq5k{Z7tL$RF@`};E z@ZZY=^rO~PmT*T{DR~$yPrFnp==vRN3!2PHu2sz{G2O!X^X3F#2uU9`q2-s4t$!$2iXK&QHj zrwBKDZHakA%g|*$VfjaX$(uv!9LR2^D4lOfWl5I&(gbsz5=bx(p8U_{t1RMiY4{vl zaUrZO8+oPuC4&@9;*4Z?NsO|dEp(P*;`_tyTYytEoznOmX6?Qq2_|U07{nGb>eDxA z(Vz->Ic^!*zYFH;IsGZ^Z&7oZF!{U!^VG_0uuH*i2tbMt(-;||-dkl!Ed|0F$kMOn zMl*?zS2=t+xNeUUr|?b}C1k_$1Ko`&D1QD*z`qZSKI*qz`d~q2FXQ>nVt&HjIa(gR z8X#;1{#Xm8m&qoJcDzuX1WnPqXD^A3uN)u%XZU{#JMXBbnnjDFs8o?6?W1?3_g3=z?q=@_$}v^ zGCc_`Te+`WJqM@YQS@Ur4?cV8h0kVYaio)QMH752zI#ji`@B{So);m`7DQ)JjC@aQ zQen*IUO93(NhNXMb>{#+f`j`0MvZU}KZl7S%*YHXo=H5XyfiqgCh_d1eLnr4MtT9@ z1@}0!IL?0p%$x%CNK$ixifZ?rg-%Q1T}sPx>azS>OWQU3(^f#)JCX^{8Kjjn0f6gt|5V&={CVc>59gN9M(92KUR4d-cgZU*+uE0?B2);*#Xr z1K^&o@?Qe^y6Q{~#xTvEyh^Z%p-E~#noVIuLMq`#t#P9jFW3arZ3j6AXNXKtHKLZZ z`bsUcIYpV#jP)SWcbh7f%6t_M^r0tszlRQO#D`Pq?Vcq47>h5w*YSc1rzEiMgAc&J z_(ZLLRTR2w@(Xm^o0ry4-l#?BgOpl?XL#bOpTdn8Pf@(X`B|zrq+=~#H)VhOCS}et z*pRtJ@F98aMcS@hrM(>u>`DVEwCJ1stIzzqgoMf@k;Hb(*{ruVqF^aecZBzeBmJJ< zwpS-5(Wd@@0_`LVV!qeKB*BMoX<{QT9wvxiD#&D60-m^VESY zli$|Dctz*p;J`Gv4Xvps#)*+77&r$RLpy?!}_PO2&mb=)ux%f zkL0f-cmhHQ2AwgqoB6xI->s8H!Xvmzz6%|;2GUpoxF=-x$@{sqhEF!TAIsns0|a{DsoZhmJ%?u=D1Wn7+EA;3rRR5$tBGATlOR(<8_ zbn;oLSdWi?XGkcjwD#vA&fkjAO@v#ZV!dH(>pDcW!wY^PdH}D3-kC? z1wuxT7d>W=Aq=@ZL1TiCYg!hvQgwCR^cpL*835ZuY5N!vUCK&k8unzVyGzRxnxtRu zk5>3cp&{H_Jzw%%z83e-nh!FzxA#DPco=OSr_4a`jzcFLD9q#0-{1U7ci| z`;U`*#Kd{BQwO#@dILEVLYV-*tm-&Tj!Q7hbWD;81?zsvQq+6@isl;^7Lu|fxqO+(!8L!9 z*EzM>$yf0>@jY9%UI0FLq=;$wzSSDD&p6Qwa;P460vXX-jwxQas&YysZ%j}@nvC)j zMQ7^Gm&I?2ZGj8B_;QGWH1lmu`Qhs5Ts4WBmn@2tP7GfmkAaeK?-uMzNTOokmWMJd zE|VE45xl=zOzbPH`?p0qmL>TMLhk#S_?^>H*u?Kag-25w$jn!Ig?k(GW)f?xtK$Qj z_&EL7K|pod{Ag7gRqp-dIuPw|CoM34mLGiDsDeJ=erp6Cxl(lmD2R=?&yjn+f{HoI z4|H%d@>Q>!fi=FZkeM4(L;Gr5{Go>evcW+ z0$>=_4xw#pDQ65S*7U#&k9a=S zK?pTnKNU;yc&}5M9J8mojc_k;D zX*pXsIvQ;NSQXVajV{J+ocQ~_h@kx`gSniapWJZ_r9(QF0wKZpO9JW9qyJ3V4Ek^k z#8O{Lrf{m4NoU59v%2=ifbrdr(my+Jyuw9KQG^+YIXLT4KSfjN6#Ld#n=47P;Ptu_am^Cc5f}4LdhBTZJ!KXmX=dwvFf>j-TmUyr<(M{d?8a z*v0sVNuu{1fYV?FzyxGx>ijE1@);O{49TgBAxR$jcFhnR!%Jt@qc{R*u4cujIeVGJ zECaXsuken5p!LAokxnih9n1UJkywxmm=Fq>luc7feDuoOYO-{1vf&gr<)D08dm)s2 zxphd3^V7KA)fZ%aD=%2rFGr=uE<{C2QMWMreC22j=;QPdVTq@@{AZFBD}QHl&E_FV z)$0bOsamR!k%7^lgkdCa+0m`-$2aJ{06@)a0KB5JYGfZFO6t*sYuxvJ#FIh$2&Pq9 z`Yfd>N6Zn*ihTpfk>&)P7qpJlsVQ)s+x=kO;*6jEvIp9}&RS1;mttgj>eouDXYbQj z=umdZC&gXk3|(==qfUGYl1m>+UC^!YnISCtGx=P%7we(_5Ouo&=$V#RsIy*C1*A6X zolXPPW0v1Vju-4g_r{AlHQ~1%N>fI5YnPUhmWlK1>GK}S>ar7D>lC~YkgHCc=OE0K z^@8On`Dl{${r`EX^`Y1!@ueiOYF4eByC*yApI?7Hgg9O3D1iX{_mu&4L0V8M5!JI?s6T&0$~yku@b*pIbLSoPLU zVD%bOxQ>lh@U8#GU@|T|0?0YlH%&4ej>YIPlu}>wn4$uaXVy0IgJ(h)1-#>Aw zb$57q+{=ta4H_l&sK*bh}%c_!tvbpuAErn9Y z=T->{QnXKH54BOUmiGneF3feB4w5LF_evH zg_NTzg4p28_V>pR)tzNOsKCcbny;vy7)J{GoKHRi4{ybE5RkU69)N0VxjBi?%Hz~T zfM$Db4yZ7H;b4H+1$GG9+PpvqswT<@Ko>=x|KnwZkq?Me|K*wjN(_K?;y?Ir1-}Wa zcm&z+*M6Vl4_oa2DZpE$ev|M1yIQ}G0c!a_3jWhc|936$MyB7eC7nas%56Z{$gqQm zugry=1}Jn1-Ce?X(F=VQ{nD`AY*pTt5%%j^txwEB(_-zc`@ty+-i3EW(Ub0tjuxfZ z5v7aDx+2^5iz7a8-(?Ycn5!INeLrN4_vF`6Uq)wa${DkaC+U8PRaHL9m-1IMR!FkM zZqMo<7i+l_r&`N-vaqRlq7vnfEz5^%H#%N;zA=Ps8n`1)NAIS!$AN&I%xuipNz8R zK^0^*3f4u=^y)+*2%+3HQknzvr~LJ6D&s+!bb+gZ(`#u-Ku1_oig5QVt04)!#HIZY z2$Y_C@SWE?=X;^HEwtxVRr_3WqA7?JI}H(Tg9`J ze!6=!ybRwzriqUhG5HQ@bzOzEcNl$kv!o81l2va%KB@B)zf?kfPd}k>ON&W%fiaA8 zXGpv2#H zq$P5avUZ>6j!oydlm2q=i}c*lM)$=VN{dF^y=jsF`{ZkS*ty4?JKCHKp|EA>20^MP zjeV}jbYV|{!brBXfZ2xJP}PTXJbE!IKb^7gu3vkIi^>9Kp9iuC1qG}RDqO4=&Nst_J$Su3$i3M(=NPo8h1>P0(5;52Kn zJY_;Cn_!-;g1>|-Du>Wf&T7h5flO6>b=#hg)bp8mN@7lK%*9C+tccV;pCzL8i{h62 zKai>O*aXKbsPY0Ob>UlcU0A49InI{o)T9Q}W@TCQ12^^))_73RoNxv^QR&keV={u% z7OBSkXv*0RIhffDJ6=u?AG;B!)BLD?Mzjjk)CKrAxHHogU~iRpE2`V&OVC9!8`yiT z3qVY92og^?WReBmIn8%#;^(Gb?w4z-p}M>Soyk}sU-QFq$JP$A1j4$fhNFFCq8A`h zn@u5y(e{g7;fTq5g)J9w+wEjVZHG(;^2LVFBW}BlD5hwbiwl|&Lk|dIU>IVzo#Lv) zMvTSS2C8X-eo=)mcEnD*-pky(ea82D!34;OHBDqxxDYPuu#jOzIzC2hZ?~WGqK3En z&Oe3*$UxTFS!R3qhM)6qkHj-v0 zFDcivQ!>(0j|U-Z{3n19RG=1d{pTa2RNqx7_YdC;scYNlL-JnCRy~=~j}s*#Ud<80 zm=`kLb0%yoJ;4-^NW0>ymhCgmCv6->urx{Md6V9{TxvjdJ{r;&p+)+ zjxhFaYq=J3kX_?eHb3K((9+RM2J<@ti|!soWt@K_D#?SrGVj9>IS6T=O zX<@W=fK>)(5;$2UK4iZ1&ihzP+4(;D$B%YaHD5Ow1_ITsO^9pv{ zZWUegwpg9$eGDyOQMEPML01U#=PSHErUeX@uJPgts;sNyHt(b0ZY`LAX>sov^5;)x zhqRgYT8SoRaKk2tkLGd|iC~gN4aCIjF>|OE4COg}v-c_b4U9|eCBxmp6#6n5`?Yn5 z-iL6N$g|S-h*pwjGEpf&nW~;r#B_|&t&fldO%xcA1#Kb}1iepZJ1B&l<+#dQ&k-?v zZ#>4$EsE#a%=($yBmTjo?Ye+@7f1Vm_kkG`-8CwWhmXFdacst&9cB2RYXT=H`#eLih`A=v7he>L zUhDPUR=44>R+{9zl^>wZfx|K`t&!sdKx}M2J)Ik%MjhE5^izip{-0|RwGK8Dg`y0~O z9I3*4i=|eEr_tV5NU{)L&?<=tI#ySvhsNw= z76&{x%xx=i{rMq+{DO#eOl$Xp)Vx4%ho>=11`f#^>Pi}yG0}5wlp-U0CDrF6{A)r< zCWi5M1=TX;-2=;O2UE5T?wOy14s7OYF3>dIVSF4CzUl4F#@69u4j zw%f)G$vcop9Br|)DFiv0I#t-^951FI8uZVMBiV=jcI=g)R&_qRA^czl`gb{gFiV2t zSVTrrbOjES+!N30=rJ`^_fB>cA37Ip|J&sRL|dKBtr)?$4<>o@4}@5?*7MpxNA5nf z2-R))y@HVkQN55U>-JWg4b-_>V;amlA1x=yQ{7j4M55Y!z_Jy*2&S17Bh`bPfwqK< z?Qeio5Ibl1>|2jBiK_WaQ{fYl8uoeY-m|e-1b3;jAN8^gNqiwiom%*(m*c0}`{>tC z3_o8Gg22=OHd}`X9#7E!qbi-GEup^xO7zoH6rD$$Q!LJAV0QRZH^Md>04A)w+YnQR z_vrWa-nJ!3y+rFPW~t$=^}2Y-$fQIaSazwkwCed(5yJ><*Lvm2zBXhs zP!36?X)-^6wK`Z}kwjAxLJ{4#b)y5T93PU>l*$fS0@q|mA1k8wUG zfv*ASlO);Q}A?L-9mv4aEJP zN8PjmdNefs&Z$JLgbQx{xAiHET>RboGg>Cp^r_YP9LVXmLd^guX!ci_Y$By=&vDSx zhQS-=)2Dt%wqHeb7I`B@J_Ku+Hq(YpZ%-z_`Pzpq0KBcNVf(d^&sJ zi@2y@Q40T5;~l~%aT^54Syl!^Ergu#lv2aslOQ1&ujb;4I-ZI6UDw#GI5ldIt zQQf%cT(ZX-ux&-O)?yRn%s4>!msL|7(eGRzGA16P{I8{*Imc# z^EpytnzsYSvLEtNC5v{g>*w5_ZaWz}8M_#96=h_HYqEG}9affazI)(9Z|?Jf20EIQ z*vbT>-;g&p_h>3C5tHY7bk38{uVMuy@6EE@M?`0K=rP%mVno-pWr1iFHnvCN;(0X= zYa_Lka3i6b#54k4KJ>i$`V2iHUlD^1G)n10ksKi_{VfkXRPDV?l~SZ(jGm-a9S5cl zr@36#4*sn{4G)u2qnn^^@y1Ki>o4Z&P@ttHe;u;OTB`;R>rQ?D-tBwUSb;Av`bT;P zSYI6{TOc@Kzo);^bZ(8qA(!uf;m|f@tYl8d*%&JWIigne>@C}lm8S2jl1Zy==dHtg=CV6rUk{!@)5wIWQuQz9NHT z%R-nW{pMW4c5J{Lq;w1sx_7q*K|B+NL^a)WM;EHIJ5N2L@cR)|pTU$a;L^|J)p3U? z+y^DU&Dhbp_;wpCRyWu;;iETgQFm8Utx1tV>-JJeqGC=T#E7a qxRdLdI2jMLDY zO1nDtVg-<$KyD-A%X+cW6Iy7AbG~coHY+N8CnrjTP`IG4N>qq;*>g#9(!uQRS=)rl*TSm34gByZMJMq7mWAqM_ zY+mM(bF!f38(+G2%Zth9sEr@)TO!+Fjx#B6H^z}|pE`S$ve#qg^L!%TWwbNaREMddel z4sss1A?lMmpaMw|4J^e$rjYWsq?49k$``KJcv$y5+i)IL%I+4M@K4-z=_C)GXGx{G z=G5C+Pnz-3Em7E0bOvg-8p;ie-$tM{1#mPrPKWX1-}mnEYW|Y z9sCjgoU9FO%Ktqi6rKoBd%(8vSIGfY`cDOa!X?1e&hY|jAHIMPzwO;{7c6Lf589vZ}Yo|IH?I>ZqDV)O)Utf89oO;+eDzNWvE%I IZvX7R06kfH{{R30 literal 10646 zcmdUVcT`hPw>F6M-a{9VAQ&J4=?E%KAb>Q9Gzmx*q&ERY>0Lk|bVY~;6zN@rgkF_U z1Zhf_-a*=Tu>9WlzTdrT-GA>|b58c`GkeZHXXZTnnLUZTrK3(w!A?OyKtQc=Q$?SE zfDjEFb!6uO4IlKN2lyfM&{w}fP}Ix0NI)QTOGD+lq3`I*xK9w%Y*X8Y-C9bp^7#bt zP5S4}G`vA?V1|UTxd^MLYQ@cAbOT>*MzCKdd3BNRX%;K56)RH$nJ@`6Gsp1)H@0b9 znQ{Hc-f?g^(%Wa(drhY3;qc7cu(yPX^p86^`u#h9=k|q37Fkd1{pV;w7af_X7ykg^JzNCtH2rLX62paAthg$b!o4@NQo1?}E` z7;upa=)#6RkSG_@$q^vH0Mvexgh0uYeiuHt@Y(fn2dNJ-39|@@(x>RVn>iux55fi; z{B_q2**qu1QJL$q@e! zMN<2-C90oG7b70rBYR(G+-*(vn4hRCpw;#A$5|psN?&!h;*r1{m-bx1Pl_c-L|q z+_RERt=kuS$Cf`SL2jS{qn@W<+tquE6;FBDVrg=XGE5>fd+cdGV@}I>&tW0%O7{I? zgh@%5u`E$^K5PW>b5p%4G#avm>Lz;#{_*$#w@(6cem58AOM+f>LjvOgK_fcVNZSqNAF& z+0E116y`R#Quq-Wboj}!gzm_I-a@m8mi~|)gw#4{T(;+`QndE!?YpbCG`Jd?uAFL0 zEvfLbG~GqIjS1VT(EY5@`SNA2ppqUU8|+KtEA+r{2()c;2DdkD zUQKyaA3RI4w%~Zd#lMeZ`lb__It{TGRS23NuZ)>_NdWg)yiQXnL%|X1z|W$kHDp=h z`6A8~OYg)Ro=sz*1o2p${Spvr!*5#rw(V0J?q11QSC-y;?#>Z}tp_*-a|OxI;V?@^ zf-KGKeB6;64NT?^nDZ2SZGTc-aXk)`*-8A?Gya1zt6W`CLyV&5cWOwVJIS$Zf^l&` z(}Cyf%lL8f(7PUTNhRI6(ku45E^~>v!Az`J8YmdUf4sPw^h%ai}vEwt^UO{`l8_Ipk@drL*%_xVp z)qc&j-Jf|n^kO?%x~5B&hAqk6jn-UU3{_rHR~<)Z>Z($lCO zoRb?aD1WqLK!49&n_JvFgPWhZg=f$pgw5mo7y1BtY~BDAUaJV;h5?)E>4FK1Dqsgj z_AAv={K1m{v@s0LU6KI0O4i$^COgj7KsnN(t9Gc#kOl0hn9Du;u-v!X+@Gf-c>t%Z zXz*rf+&k^uYY9<-^@3sdg*Ohrnx27V!SaX-NZB-MfC-z&iBO|&we;1}=7o1YxhG8x z1?kJ0$Ns7)2u<*wusYHp^))kYjQT#un&pzvgY(P%_`lD1P%I5x#O6I09lz=IOdl3f zqpHmZ*K{Z)2 zR;`R~9s9&0U99|B}Ec?(V)`6G7?CL<98Dx5C=eR{P znRngSGBOF{9)7rHX~(oo!}jXRq&Ojb&P?(dRH$Q4!1i!e)#|A_q5#YQ7N}9Niflc@ zXZ_k{EBz1+dax>h?Es?xlK*u^0XY9x56BEavtOEjQ08ZdE~p4nC3GB-tqU?A?aD}xoG{}!P_G|qfN2^)>HLsKZCp_u2Y zBLc#|%}{Fx2(B|Tr(Bn7WPc|X29VAUa)VjnI|^?AfJ};NAmoJ$V7Eo9RJ0el++5C` zdRG>=p<++1^+h%7JPm03%Dr zw3SFU-Ko%9vhk8sd5S5LX1Xq-(;kp5P!CT{T+6Dz*PaK=qgby3)WSsKE|yX3^i6{V znm`JSQ9laH6z5iW!ogmv-aEV2oh$IF_}&Kf_F zOX%C^hdcJ7Q;$7;P~yH%PF5vpKSe!W7b+|9oMD^8hxQeB&z`qkmwcbpWA&)(6=cpI zlClPB4Mae?>~WCRerhci%vei!`31%<2?&x~y%b7;w=rv<^h^JVNZDXugQJ;KFax&2 zV(xE)T}jvQDFS*G3!SO~*!R9%UG}|pih=dc@1TR0@T_8l+-7J-|0n0`=?S4W#Z?)@ zIIq0a>I;$M_56ORk(t=;H^zI@ToA?&Mbh|iOio=z5DS%^%+(s=E?4fzx{N*JaEJP@ zQk7QJcLtD*DP_K-U3{&unB97&;Ke&M^Zq~ToF|XRZkkI$lY0>m!3~imjc@*tVVGh8 zi@6_1*Ktg9f2vQh0zVvkB~}pu`B4AhBP(7muF9B;u01=#OrKEM1WSIxIcK2$D^9C08n4So~d>ED4^(qD`Nt zi&JicVN2$D1w3DjMa##wEc@Y%JEMR218oyL&!q7cc%Tmud zEq56a61;faEa}^?gQGXK+yl5?$I}`}8N2%o6skj6x^e(V79TjD1=*^Yd5}t6Regd2 zQ;>t`)pjJaI+7pyLF+CgZNLZRE)x|?gP)KcXcLd}FYazG{u4`EY=^A($-2^xszC56 zcj=UuZcreSNK)VTbg^yEt5`IZKDoc3saxf> zAM@Wv;839q?(M0II$kKq>C8RR7;O2oqy-J0^pX@z(bZPVJG#n`H0;8jaT!r6p;gV= z$<1ae?SGP}P`8%*%tItxf3gg<1@`E>_#1APf-!a?JC(^P+8UXOy)j#Hw*_`|_Ie%* zrxr(2ui{<`$iCJ7J}9fe;?t~&D&PViXA9McCu;??$%`cUw!nL5^dI^Ykzx&3V2evC z9m(*?Dk=Bx=OQ7|2PSttnZe9~_sz~Q@K>P+GoMAr5RG2}6#(agfX_Q=z@^ShKh=$Y zQTa1M&-~{=7C8M1z083`znTBy1@&K+!et8_bZ3$JSFtgg0Ompge55D-s67J}(I3sj zuW1u-^x z6ul4_XhPqBEXmzc{o#K^+@_=dKH^^azY%e_=kH+iE@s?>d`8g$AyG3G0gpN(F+ijd z0$$>o{I4^465l_XGx@C9ndV>eZ_OX`UswO>>i_HKcW-}e4O(WLVl;`9>q7Etkw2QQ zBvV`x3B~+GvgaDz=9RSoX2zct-S2hXUq*1Y#ycx#k^irJCj5@)zcmHd;C1aUE?NG3 zAax#Y-gSHO1E*73eU+JbyWE?KzVG3Ui`OOF1%d-6kKwz(V+#a3fWgIO@R#0=1G_Q1 z;~ecgQRZ_#0qM>1b?GFWSZdOWU1L|BsqGi+0inoPpZ5efQ&K}MWWffC#ohDJ$2z zX}N5+zq-)7McF=0KdPqi`dPq>i2MewPWbHuZ&i=h2ZK81iL4MT@nL0*O0FTX4iFV8CzXt{oEX(Ikq7h(=@b60xA)7_&{}(vPM2P@5Aef zzkI=POl~lLd=b9qR~OD2&<4jjG}Ya6=WWG3Z{sG}kCRbsUccnWw9azZU;bJkL&N*e zoQ!Ud6})6aUl(^<%YI6}yD5A5fV$_)?1Z`E!HgR{*8w>~T~2zF=(2384wD=_@J=hk zXswSgaTq-Ute5KcM6&&ro?xE}ce!EyDx80K_OzRRrZ|`j z(pdZnIZ8PtCnpO>dJ}z$e(*#R6^j%lcm;h+qd-`aWcXkRmIP%EcUar#PX#GbG?p{t zwpLNO7G`jD4`(){<_6bDeJnCfUcUgRRKK{E(E50MrR*ke&t*I(+DUrv6&Lz$wcX_P zvPO3J#ZWGS?uh6`t(Yo9U?p=6&SqhJAVYxnc3HZ03xdAiNoK$}`JoC1@j^NJqi%at zyGSoN^+u=9`-8k&n>^)vi0-eKhdVqhLD^CQbGj-cm909%M`oC4UAv^tPsDoe>eC`U zgIf2CIL#mj@-REX{eiDNUsXv-4zfKcmEzgGX|UywrPs+1S zx)9CVZMinE9~1HM^q3$BbLAzAL%C%_M&AR`Z|f0BtpaW3nKXK@TTR z7N^Skli8-~YGX}Z{dPo!L?4wF`oJvm*IGW*egCM1Xpg$s5T4q_AeAyTP6>SPd7Sls z@bq_FIy?VG*gu-zSot?_{ldy$$oM~L&ffy<#jAJ9)zGts0mvw<3|Gs48U;0yfN!C0 zBWL}>JSZxlpogi`?xaX)o-3d6rV8=UA$UyfNZ~8;NQ8J<|c{ll#@lBY&oD)m29pyZ98cgxONfUR^5{2k!BmX+y~8lW6Z} zT&DXY>7|?G0X$wcC|3TWzCtpC{LbN{*d*D^p92C}asy}<<9;DT zOQ;$G8^AQ;j&rP`5JFqG^FF28h3cpR9d|YrkqH7zMiNG^$g<5IN)XlBx#BRE%=SGK zRji?8X_m#no+#pgIP4ZyYlyRZ<}=f@s5{A$>=XVEl%+yg!^pBHNe;ck6UW0as<%^n z+pf(W;m+H6ifax=z~Zde%EW{7exSigEbUsExMPfxmzosB+U&)@C z!eF+2LNC9=jprSN{A3l!9+xi}UCthg`OYX?V91<=doL4HW0H(DEQ#bcDo8(woJ?$u zHp#*oxR*ydl^W0Mkih?E!8=Y2Z+9aPV5(sS@q{AB44rJ=(&ukL#p1>2> zZ=f{O)1?ZY!@&S_9OZ@4p%e4KXZ_1wnWB0NTu#*1<^ueUwr?qUtEWt6AdcyHrP;xy zqVjlM=g;@SW8mBll3DiG(1vWKDK+ z0b|s;T)q5$8?-0Y(|Z~8FVybW1%%5a()wdN6!k*HDE6(ngpzA|4ZWIU&)l;r0FkL`jwIR z+Y-ZqD&6OGeQ4XLyHR>)t*$DAnsndRxP;%*aXo-PHOnLD6k+0xdb)H=am84c@lIDi zIF)KM+E1Z+XQ;G-%3mWAX5J)AXg~?AXa1Bgcetwk()+2jM6_3nqD8z0p+WFGN6qq+ zBi<&8EQ{rWOY-n0uS+?*{wP8VzvT;zY&bU0Z@U*yS`2> z7p}W5RoHp5?Iwz2&p4INCpcw|$o1eWI_vM9o48=U4GCS3;#i}zw#?woc7}e|!a2Kj zsJXGl&MwmA{*^q5FhPgjAoo^JCCpgQC6Zg=91N8f7tus30{PA5Da%%@hy0f>X?}sr z1}v>?14$tM)+BX{v5&B#wu=;q^eKE%UYF>__z{p)66>B=Q74PR$S`jWaEBroe)G~f zsr?fNg`E6`3e(01?`IAzDw_9BRS%fXUH)LalE+PZZXFyZ;Fii{+SPt=gZJPXy;fkB z?`18%Ap_p7FDtm`bLi7ZlbKfMI=ED;Wt0y6eZ;C`7Fk@P)LIOC$?XtBT&a4Lc(Pc{pQ9{upURe@NOkm^ydAzXF% zZr(Gj48cU}Ix|7t%~>9|6ejboF9%rPbZ9gh)Q=!=`VOIT;OsQqi#fQ$4`=sjwq)0o zAmy#ysq80JMs?ZGBcoOX#QITJ9PycJiOT}{jrTxLY#6-E@lD*1wpX?u$9#KKxg~bP z!tRnKt=Rsq$(}2dT?sK_^=rA5?Tx47^(05(PDKK--Hit%BJ%N9sn%DhNk#l=&|iZ4 zN}HL?!#D4LjCQ~p72SJiUVqi6I7+gf^6kZLFQ;$LCT{tMp1p!O0q9$8$0esS{b^Wk zTYTmr7upuy=FYa$L7d%MJR(v?FHI^cRGCt;6}mAn%an~3kDt9pTf5FCe?W?4A@VCn zE8X7@^5#Y}g?SK}%Ve{;nYhS^_@=@|n|)Ea{vF{UZh`o76PrT;xfc2qe~BUKr{Pb{ z<3Ihzh!$9(0s2wa=&kW5Qw_Qu3kR*Hm8YM??MG()x|s~aHyzIWA1qpK^UPjj^GW)y z=;iW~g?M6n?M++Rs`vdOTS56RO*iAbQM;9^hoIYpv{v;VNzUWepS7ai*(?Vj6PFnN2C=B^hD`#4^#Q_X7g_kr^Xddhn&a+YfRr; z)<4FLcfux`j&!Jy{bm?#LRZi|u{@obPjraetLR)UvUU$Kc4dP6gde-sHDXDrT;PUIh% z@z+^GE!|VGHqmZxLPQ{KrxLC1kH}5^M;QNpMbRVMIdN4E>4Zh-*W&D3&d7pz~COzqZ? zb*mDUyL9W(N-791j5WjAqVfG%LB2`QwP)H=Bdap@oYWw78oz)AI#uhZ6P#_1Quz3x zxO>~cBe2O&+QUC36?I?8aaL$McOmE1LYw2T!Ei z+t8NX#CT{7Z)$+8?_uBm4EWOOle;C$Z^(S{>2!4&RA&Sy?;bTRQ7tyiFOP>? zv^s!jUP@Vi@O!Lb*B_W8Sb_?}M;Ow4%dg&wwb+e`Fi7WVHs75t2j`pV#HtU!JQ@{) zfl)V(BMhb=)5u7k(0O50QMfUOH3a;QC(cr)mBS{=9GG#|^BmC*j6~!KaWAh@jtv#a zVg`_TK2m*_WS?VBCMx9tMEcCCjB1*|Y`m}#PX-V0#j-#&OH-e3Mi(tH1VlTqveDn2 zBkR4*Qc@PeKTZr0v%R!xDN^V3alr)UbxDJA{$fBc#ck%pLG~}1>+(J&6>!^zI>b!n z;vtE%@$>Ht)Jo5io6Ck0zRAB%yYzF&m)KIjzz#@Y{SBzU(ii`$(EEF%<9Ej6cWf@$ zUuHs!YrH(<5uPQ0u_Xhf0`B-e9dQo!&ekMZ^c@uR<8y|5n!eiB*zI%hNu!Yz|MaCr z6;SX?UFJ37*}E3xSi`}s$01{ap}bkqY3%cTY0r=EwBkGkR46d3Yo)i}=xOHs2o{P* zH^jOK^J2HZb;zXBy&Dm_$6e*l)ao3+o8rolDrDez9_-s5*r1krS3G^zWRPI_yH}Vi zBJ=q46!Rp+!KG6F>k^Alh{h`^ubcC9G}ErVQ+jLcw2~5cH;rpuIag#7`R#A;)4Wo| zfNp^F7x;iBrV{SWY+QO=;WyXB9@Bvx#UKk~4>J78TW|<(?4*ZykCvxZo$VaCeoeYH z3^dyOL9|GpAJ%jbLcU>IiI4u-Jwy?g!S|reRqPXiGKFq|Mu?F377QduR6~`U8av9M z_c0Fqx|98h(_WjZC>kBd{&xIhhig8S1+M_ct*OoCm#!l6h{rC~m>w;zsSWb0rNueR zff>-jIX|v$KJVkvm?igeMGEH_PA0t0%2XY1{6+}$qhB!Jj#rcW{G9m57}l@>^N3$p z%O2N<$S)FsGbjAGvboJmEgS3f&JbDf#xOPE_35n)%AKzw$bv71;R&z9$zeA>3bUZ2 zbO*WpgzsGvf+;EJg{i0;fRfNpMsgvPEtxt#(v+BIBF zczxpmU$gx?ARo+*spr@AuZ|e4z4QcXlyBIZ@Y+e|H7}3j4w=EA<}8PwNlTwSj^Q0U zu$IJ_@#|9BYYIv^Rsa{_vuu73qiZu;blx!_3pz1WAN@xCiAMw8Q6UR{V7?^0J~n<~ z6oO&T`>vMlesHzI_j=(qRDrIr?To|at8e-q4<2C+Kj@$E>n5DFTMTl)td`G^oZ5mG zww6nG5Khh`Ghl8TxO2InU*&(7@%+Wv|76gAB|Xn%%n{UPLwRbuJLuH0T>!Udcd>5d zR-9zJZ6I5>Kq*NHd6B_PsYfx8WVl}Q)Z-kQ#nSh(?GJ!u`|hD{$v5fSm54S;w7Qa* zS7+rg3|Zbwb28l17IeIQ{`B!hV57r;SGr7w5FI*} zVu^`OLo82MCG~=`^JIxS*$ha+COxE$`x9S{WWUh)&CShQ&8SR6F>_WXTHYn^#qP1y zy@IyP39|UtY52{2j@Xz8&46O6bcteSF7_khoElC21ag)J_qVE4Muw;cW4T zJvi2RuTCXk(z@S8YfMYD%s1jos(tH<6w`oo{o zbS1EPZsFk$=Xh`A9nO_Szs%E*r~44(On>UT?m3k>*}9(W?<{=kYT)liByGTf>m%^} zL%B&j?6pyJT!?SznAhZG_EEgC#MgND64gX*m}x(-q~5juNq{?a`@vMVtQbP(RDjn%(Rt# z#!|;0H1}|$Z)f5-AqHdK)4wQl5y{fcd9_ED0`vVH&ReYpsRq6jT?#2g(>LuX{88E- ztF&*glZ1JIdS%O}#F&VOguF+d{mQzg?f&7|D@Q#|7iCm=;e#v&sPQ5WA4&y6*dO}H zp*93R4dO=JJ@%)-|}Uzq#94&NGz`-vyB6TPj?u8(b1Ax|1lHK&knb zZio6R>XC2<_W>FkPhg~;dK{QrX8i<*ViJIj^-pKa5H+#qop@rh{9qyiFe zlEB#k@o|yl9bRO$La{|G%CW1jIF4ic#$h@N@K5OkK_GTDCnln&6!fw}`8 zaMOM|)M%QK4%hL`??)}3n0Tg?19yKWk#xp`=@`+&IrwwhenCA1Z$dfev zPo@T^lt-;^3MN~)L76xI&@-XE1@?Q@dyg)0)q9{yyrRyS9pfYqL2(Qo%6Y@N^=BN3 zX;QAiv?ML#>Q=*kQcdXFo8N0SYal`!vm(-hN4?{s;#S_vQYA7?gQpaTCgI89hlz#P z#k=BqZnGpWn1o;r-@$}t*?#T({DMCKF3-wew)Ic-Z*t@J?$96p^=~^x|DnmJuo4$e zegnX8>ozssP+0H4!l^(MtP1=E1t@wU8hXN1_*0@ByYss56od_cKUxxKsOqQ`-LQQ6 F{{SUe+a&-1 diff --git a/_static/type-hierarchy.txt b/_static/type-hierarchy.txt index e8ec6ad..19cf03c 100644 --- a/_static/type-hierarchy.txt +++ b/_static/type-hierarchy.txt @@ -1,11 +1,17 @@ # type-hierarchy.png image is generated from this file by ../build-docs.sh graph { flow: up; } -node { font: Menlo; } +node { font: Menlo; color: blue; } -[ bidict.BidirectionalMapping ] { fontsize: 0.7em; borderstyle: dashed; fill: #eeeeee; } -> [ collections.abc.Mapping ] { fontsize: 0.7em; borderstyle: dashed; fill: #eeeeee; } -[ bidict.frozenbidict ] -> [ bidict.BidirectionalMapping ] -[ bidict.bidict ] -> [ bidict.frozenbidict ] -[ bidict.FrozenOrderedBidict ] -> [ bidict.frozenbidict ] -[ bidict.OrderedBidict ] -> [ bidict.bidict ] -[ bidict.OrderedBidict ] -> [ bidict.FrozenOrderedBidict ] +[ collections.abc.MutableMapping ] -> [ collections.abc.Mapping ] { fontsize: 0.7em; borderstyle: dashed; fill: #eeeeee; color: black; } +[ bidict._abc.BidirectionalMapping ] -> [ collections.abc.Mapping ] +[ bidict.bidict ] -> [ bidict._abc.BidirectionalMapping ] { fontsize: 0.7em; borderstyle: dashed; fill: #eeeeee; } +[ bidict.bidict ] -> [ collections.abc.MutableMapping ] { fontsize: 0.7em; borderstyle: dashed; fill: #eeeeee; color: black; } +[ bidict.frozenbidict ] -> [ bidict._abc.BidirectionalMapping ] +[ bidict.frozenbidict ] -> [ collections.abc.Hashable ] { fontsize: 0.7em; borderstyle: dashed; fill: #eeeeee; color: black; } +[ bidict.FrozenOrderedBidict ] -> [ bidict._abc.BidirectionalMapping ] +[ bidict.FrozenOrderedBidict ] -> [ collections.abc.Hashable ] +[ bidict.FrozenOrderedBidict ] -> [ collections.abc.Reversible ] { fontsize: 0.7em; borderstyle: dashed; fill: #eeeeee; color: black; } +[ bidict.OrderedBidict ] -> [ bidict._abc.BidirectionalMapping ] +[ bidict.OrderedBidict ] -> [ collections.abc.MutableMapping ] +[ bidict.OrderedBidict ] -> [ collections.abc.Reversible ] diff --git a/bidict/__init__.py b/bidict/__init__.py index 8c6e348..dea8382 100644 --- a/bidict/__init__.py +++ b/bidict/__init__.py @@ -48,9 +48,9 @@ from ._dup import DuplicationPolicy, IGNORE, OVERWRITE, RAISE from ._exc import ( BidictException, DuplicationError, KeyDuplicationError, ValueDuplicationError, KeyAndValueDuplicationError) -from ._frozen import frozenbidict +from ._frozen import BidictBase, frozenbidict from ._named import namedbidict -from ._ordered import FrozenOrderedBidict, OrderedBidict +from ._ordered import FrozenOrderedBidict, OrderedBidict, OrderedBidictBase from .metadata import ( __author__, __maintainer__, __copyright__, __email__, __credits__, __license__, __status__, __description__, __version__) @@ -77,10 +77,12 @@ __all__ = ( 'KeyDuplicationError', 'ValueDuplicationError', 'KeyAndValueDuplicationError', + 'BidictBase', 'frozenbidict', 'bidict', 'namedbidict', 'FrozenOrderedBidict', + 'OrderedBidictBase', 'OrderedBidict', 'pairs', 'inverted', diff --git a/bidict/_bidict.py b/bidict/_bidict.py index 4b3f01b..98397d4 100644 --- a/bidict/_bidict.py +++ b/bidict/_bidict.py @@ -31,14 +31,13 @@ from collections import MutableMapping from ._dup import OVERWRITE, RAISE, _OnDup -from ._frozen import frozenbidict +from ._frozen import BidictBase from ._miss import _MISS # Extend MutableMapping explicitly because it doesn't implement __subclasshook__, as well as to -# inherit method implementations it provides that bidict can reuse (namely `setdefault`) -class bidict(frozenbidict, MutableMapping): # noqa: N801; pylint: disable=invalid-name - """Mutable bidirectional map type.""" +# inherit method implementations it provides that bidict can reuse (namely `setdefault`). +class _MutableBidict(BidictBase, MutableMapping): __slots__ = () @@ -172,6 +171,12 @@ class bidict(frozenbidict, MutableMapping): # noqa: N801; pylint: disable=inval self._update(False, on_dup, items) +class bidict(_MutableBidict): # noqa: N801; pylint: disable=invalid-name + """Mutable bidirectional map type.""" + + __slots__ = () + + # * Code review nav * #============================================================================== # ← Prev: _frozen.py Current: _bidict.py Next: _ordered.py → diff --git a/bidict/_frozen.py b/bidict/_frozen.py index 0bad4e6..4d9da86 100644 --- a/bidict/_frozen.py +++ b/bidict/_frozen.py @@ -49,12 +49,8 @@ from .util import pairs # BidirectionalMapping provides that aren't part of the required interface, # such as its optimized __inverted__ implementation. -# pylint: disable=invalid-name -class frozenbidict(BidirectionalMapping): # noqa: N801 - """Immutable, hashable bidict type. - - Also serves as a base class for the other bidict types. - """ +class BidictBase(BidirectionalMapping): + """Base class implementing :class:`BidirectionalMapping`.""" __slots__ = ['_fwdm', '_invm', '_inv', '_invweak', '_hash'] @@ -202,13 +198,6 @@ class frozenbidict(BidirectionalMapping): # noqa: N801 """The object used by :meth:`__repr__` to represent the contained items.""" return self._fwdm - def __hash__(self): # lgtm [py/equals-hash-mismatch] - """The hash of this bidict as determined by its items.""" - if getattr(self, '_hash', None) is None: - # pylint: disable=protected-access,attribute-defined-outside-init - self._hash = ItemsView(self)._hash() - return self._hash - # The inherited Mapping.__eq__ implementation would work, but it's implemented in terms of an # inefficient ``dict(self.items()) == dict(other.items())`` comparison, so override it with a # more efficient implementation. @@ -449,6 +438,19 @@ class frozenbidict(BidirectionalMapping): # noqa: N801 return not self == other # Implement __ne__ in terms of __eq__. +class frozenbidict(BidictBase): # noqa: N801 (class names should use CapWords convention) + """Immutable, hashable bidict type.""" + + __slots__ = () + + def __hash__(self): # lgtm [py/equals-hash-mismatch] + """The hash of this bidict as determined by its items.""" + if getattr(self, '_hash', None) is None: + # pylint: disable=protected-access,attribute-defined-outside-init + self._hash = ItemsView(self)._hash() + return self._hash + + _DedupResult = namedtuple('_DedupResult', 'isdupkey isdupval invbyval fwdbykey') _WriteResult = namedtuple('_WriteResult', 'key val oldkey oldval') _NODUP = _DedupResult(False, False, _MISS, _MISS) diff --git a/bidict/_named.py b/bidict/_named.py index 5c6049d..cb706b0 100644 --- a/bidict/_named.py +++ b/bidict/_named.py @@ -9,26 +9,36 @@ import re -from ._frozen import frozenbidict +from ._abc import BidirectionalMapping from ._bidict import bidict -_LEGALNAMEPAT = '^[A-z][A-z0-9_]*$' -_LEGALNAMERE = re.compile(_LEGALNAMEPAT) +_REQUIRED_ATTRS = ('inv', '_isinv', '__getstate__') +_VALID_NAME_PAT = '^[A-z][A-z0-9_]*$' +_VALID_NAME_RE = re.compile(_VALID_NAME_PAT) +_valid_name = _VALID_NAME_RE.match # pylint: disable=invalid-name; (lol) + + +def _valid_base_type(base_type): + if not isinstance(base_type, type) or not issubclass(base_type, BidirectionalMapping): + return False + inst = base_type() + try: + return all(getattr(inst, attr) is not NotImplemented for attr in _REQUIRED_ATTRS) + except: # noqa: E722; pylint: disable=bare-except + return False def namedbidict(typename, keyname, valname, base_type=bidict): - """ - Create a bidict type with custom accessors. + """Create a bidict type with custom accessors. Analagous to :func:`collections.namedtuple`. """ - if not isinstance(base_type, type) or not issubclass(base_type, frozenbidict): - raise TypeError('base_type must be a subclass of frozenbidict') - - for name in typename, keyname, valname: - if not _LEGALNAMERE.match(name): - raise ValueError('%r does not match pattern %s' % (name, _LEGALNAMEPAT)) + invalid_name = next((i for i in (typename, keyname, valname) if not _valid_name(i)), None) + if invalid_name: + raise ValueError(invalid_name) + if not _valid_base_type(base_type): + raise TypeError(base_type) class _Named(base_type): diff --git a/bidict/_ordered.py b/bidict/_ordered.py index c886b7a..8eeac1a 100644 --- a/bidict/_ordered.py +++ b/bidict/_ordered.py @@ -31,7 +31,7 @@ from collections import Mapping from ._bidict import bidict -from ._frozen import frozenbidict, _WriteResult +from ._frozen import _WriteResult, BidictBase, frozenbidict from ._marker import _Marker from ._miss import _MISS from .compat import iteritems, izip @@ -43,11 +43,7 @@ _NXT = 2 _END = _Marker('END') -class FrozenOrderedBidict(frozenbidict): # lgtm [py/missing-equals] - """Frozen (i.e. hashable, immutable) ordered bidict. - - Also the base class for :class:`OrderedBidict`, which adds mutable behavior. - """ +class OrderedBidictBase(BidictBase): __slots__ = ('_sntl',) @@ -74,19 +70,19 @@ class FrozenOrderedBidict(frozenbidict): # lgtm [py/missing-equals] # they map key→node and val→node (respectively), # where the node is the same when key and val are associated with one another. # To effect this difference, _write_item and _undo_write are overridden. - # But much of the rest of frozenbidict's implementation, - # including frozenbidict.__init__ and frozenbidict._update, + # But much of the rest of BidictBase's implementation, + # including BidictBase.__init__ and BidictBase._update, # are inherited and reused without modification. Code reuse ftw. - super(FrozenOrderedBidict, self).__init__(*args, **kw) + super(OrderedBidictBase, self).__init__(*args, **kw) def _init_inv(self): - super(FrozenOrderedBidict, self)._init_inv() + super(OrderedBidictBase, self)._init_inv() self.inv._sntl = self._sntl # pylint: disable=protected-access - # Can't reuse frozenbidict.copy since we have different internal structure. + # Can't reuse BidictBase.copy since ordered bidicts have different internal structure. def copy(self): """A shallow copy of this ordered bidict.""" - # Fast copy implementation bypassing __init__. See comments in :meth:`frozenbidict.copy`. + # Fast copy implementation bypassing __init__. See comments in :meth:`BidictBase.copy`. copy = object.__new__(self.__class__) sntl = _make_sentinel() fwdm = {} @@ -247,17 +243,31 @@ class FrozenOrderedBidict(frozenbidict): # lgtm [py/missing-equals] return all(i == j for (i, j) in izip(iteritems(self), iteritems(other))) def __repr_delegate__(self): - """See :meth:`bidict.frozenbidict.__repr_delegate__`.""" + """See :meth:`bidict.BidictBase.__repr_delegate__`.""" return list(iteritems(self)) + +# FrozenOrderedBidict intentionally does not subclass frozenbidict because it only complicates the +# inheritance hierarchy without providing any actual code reuse: The only thing from frozenbidict +# that FrozenOrderedBidict uses is frozenbidict.__hash__(), but Python specifically prevents +# __hash__ from being inherited; it must instead always be set explicitly as below. Users seeking +# some `is_frozenbidict(..)` test that succeeds for both frozenbidicts and FrozenOrderedBidicts +# should therefore not use isinstance(foo, frozenbidict), but should instead use the appropriate +# ABCs, e.g. `isinstance(foo, BidirectionalMapping) and not isinstance(foo, MutableMapping)`. +class FrozenOrderedBidict(OrderedBidictBase): # lgtm [py/missing-equals] + """Frozen (i.e. hashable, immutable) ordered bidict.""" + + __slots__ = () + # frozenbidict.__hash__ is also correct for ordered bidicts: # The value is derived from all contained items and insensitive to their order. # If an ordered bidict "O" is equal to a mapping, its unordered counterpart "U" is too. # Since U1 == U2 => hash(U1) == hash(U2), then if O == U1, hash(O) must equal hash(U1). + __hash__ = frozenbidict.__hash__ # Must set explicitly, __hash__ is never inherited. -class OrderedBidict(FrozenOrderedBidict, bidict): +class OrderedBidict(OrderedBidictBase, bidict): """Mutable bidict type that maintains items in insertion order.""" __slots__ = () diff --git a/docs/addendum.rst b/docs/addendum.rst index 4e78448..a325566 100644 --- a/docs/addendum.rst +++ b/docs/addendum.rst @@ -29,7 +29,7 @@ Terminology but technically values are also keys themselves. Concretely, this allows bidict to return a set-like (*dict_keys*) object - for :meth:`~bidict.frozenbidict.values` (Python 3) / + for :meth:`~bidict.BidictBase.values` (Python 3) / ``viewvalues()`` (Python 2.7), rather than a non-set-like *dict_values* object. diff --git a/docs/api.rst b/docs/api.rst index 6e5b51d..510bc05 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -15,7 +15,7 @@ bidict :show-inheritance: :undoc-members: :exclude-members: __abstractmethods__,__dict__,__module__,__weakref__ -.. :inherited-members: + :inherited-members: .. autodata:: bidict.RAISE diff --git a/docs/frozenbidict.rst.inc b/docs/frozenbidict.rst.inc index f45b465..49dcf45 100644 --- a/docs/frozenbidict.rst.inc +++ b/docs/frozenbidict.rst.inc @@ -1,10 +1,15 @@ :class:`~bidict.frozenbidict` ------------------------------------------- +The simplest bidict type that implements +:class:`~bidict.BidirectionalMapping` is +:class:`~bidict.frozenbidict`, +which provides an immutable, hashable bidirectional mapping. + As you would expect, attempting to mutate a :class:`~bidict.frozenbidict` -after initializing it causes an error:: +causes an error:: >>> from bidict import frozenbidict >>> f = frozenbidict({'H': 'hydrogen'}) diff --git a/docs/learning-from-bidict.rst b/docs/learning-from-bidict.rst index fc8aa81..df7859a 100644 --- a/docs/learning-from-bidict.rst +++ b/docs/learning-from-bidict.rst @@ -37,7 +37,7 @@ Python's data model - Using :ref:`slots` to speed up attribute access and reduce memory usage - - Must be careful with pickling and weakrefs, see ``frozenbidict.__getstate__`` + - Must be careful with pickling and weakrefs, see ``BidictBase.__getstate__()`` - Making an immutable type hashable, i.e. insertable into :class:`dict`\s and :class:`set`\s @@ -65,7 +65,8 @@ Python's data model used to hash :class:`frozenset`\s. Since :class:`~collections.abc.ItemsView` extends - :class:`~collections.abc.Set`, :class:`~bidict.frozenbidict` + :class:`~collections.abc.Set`, + :meth:`bidict.frozenbidict.__hash__` can just call ``ItemsView(self)._hash()``. - Why is :meth:`collections.abc.Set._hash` private? @@ -102,13 +103,13 @@ Python's data model is more Pythonic. - Any user who does need exact-type-matching equality can just override - :meth:`bidict’s __eq__() ` method in a subclass. + :meth:`bidict’s __eq__() ` method in a subclass. - If this subclass were also hashable, would it be worth overriding :meth:`bidict.frozenbidict.__hash__` too to include the type? - Only point would be to reduce collisions when multiple instances of different - :class:`~bidict.frozenbidict` subclasses contained the same items + types contained the same items and were going to be inserted into the same :class:`dict` or :class:`set` (since they'd now be unequal but would hash to the same value otherwise). Seems rare, probably not worth it. diff --git a/docs/orderedbidict.rst.inc b/docs/orderedbidict.rst.inc index fad0451..d21cfde 100644 --- a/docs/orderedbidict.rst.inc +++ b/docs/orderedbidict.rst.inc @@ -32,6 +32,7 @@ The additional methods of :class:`~collections.OrderedDict` are supported too:: >>> element_by_symbol['H'] = 'hydrogen' >>> element_by_symbol OrderedBidict([('He', 'helium'), ('Li', 'lithium'), ('H', 'hydrogen')]) + >>> element_by_symbol.move_to_end('Li') # works on Python < 3.2 too >>> element_by_symbol OrderedBidict([('He', 'helium'), ('H', 'hydrogen'), ('Li', 'lithium')]) @@ -101,11 +102,10 @@ For order-sensitive equality tests, use False Note that this differs from the behavior of -:class:`collections.OrderedDict`\'s ``__eq__``, +:class:`collections.OrderedDict`\'s ``__eq__()``, by recommendation of Raymond Hettinger (the author) himself -(who said that making OrderedDict's ``__eq__`` -`intransitive `_ -was a mistake). +(who said that making OrderedDict's ``__eq__()`` +intransitive was a mistake). :class:`~bidict.OrderedBidict` also comes in a frozen flavor. See the :class:`~bidict.FrozenOrderedBidict` diff --git a/docs/other-bidict-types.rst b/docs/other-bidict-types.rst index 07ab863..3cc69fe 100644 --- a/docs/other-bidict-types.rst +++ b/docs/other-bidict-types.rst @@ -5,35 +5,27 @@ Other ``bidict`` Types Now that we've covered :doc:`basic-usage`, -let's look at the remaining bidict types -and the hierarchy they belong to. +let's look at the remaining bidict types. -.. _bidict-type-hierarchy: +.. _bidict-types-diagram: -``bidict`` Type Hierarchy -------------------------- +``bidict`` Types Diagram +------------------------ .. image:: _static/type-hierarchy.png - :alt: bidict type hierarchy + :alt: bidict types diagram -At the top of the hierarchy of types that bidict provides is +The most abstract type that bidict provides is :class:`bidict.BidirectionalMapping`. This extends the :class:`collections.abc.Mapping` ABC -with the -:attr:`~bidict.BidirectionalMapping.inv` -:func:`~abc.abstractproperty`, -as well as a concrete, generic implementation of -:attr:`~bidict.BidirectionalMapping.__inverted__`. -:class:`~bidict.BidirectionalMapping` also implements -:attr:`~bidict.BidirectionalMapping.__subclasshook__`, +by adding the +":attr:`~bidict.BidirectionalMapping.inv`" +:obj:`~abc.abstractproperty`. +It also implements +:meth:`~bidict.BidirectionalMapping.__subclasshook__`, so that any class providing a conforming API is considered a virtual subclass -of :class:`~bidict.BidirectionalMapping` automatically. - -Implementing -:class:`~bidict.BidirectionalMapping` is -:class:`~bidict.frozenbidict`, -which provides a hashable, immutable bidict type, -and serves as a base class for mutable bidict types to extend. +of :class:`~bidict.BidirectionalMapping` automatically, +without needing to subclass :class:`~bidict.BidirectionalMapping` explicitly. .. include:: frozenbidict.rst.inc diff --git a/docs/polymorphism.rst.inc b/docs/polymorphism.rst.inc index 06a4a41..d4ad852 100644 --- a/docs/polymorphism.rst.inc +++ b/docs/polymorphism.rst.inc @@ -3,55 +3,98 @@ Polymorphism ------------ -Note that none of the bidict types inherit from dict:: +a.k.a "Know your ABCs" - >>> from bidict import bidict - >>> isinstance(bidict(), dict) +You may be tempted to write something like ``isinstance(obj, dict)`` +to check whether ``obj`` is a :class:`~collections.abc.Mapping`. +However, this check is too specific, and will fail for many +types that implement the :class:`~collections.abc.Mapping` interface:: + + >>> from collections import ChainMap + >>> issubclass(ChainMap, dict) False -If you must use :func:`isinstance` to check whether a bidict is dict-like, -you can use the abstract base classes from the :mod:`collections` module, -which is the proper way to check if an object is a mapping:: +The same is true for all the bidict types:: - >>> from collections import Mapping, MutableMapping + >>> from bidict import bidict + >>> issubclass(bidict, dict) + False + +The proper way to check whether an object +is a :class:`~collections.abc.Mapping` +is to use the abstract base classes (ABCs) +from the :mod:`collections` module +that are provided for this purpose:: + + >>> from collections import Mapping + >>> issubclass(ChainMap, Mapping) + True >>> isinstance(bidict(), Mapping) True - >>> isinstance(bidict(), MutableMapping) + +Also note that the proper way to check whether an object +is an (im)mutable mapping is to use the +:class:`~collections.abc.MutableMapping` ABC:: + + >>> from collections import MutableMapping + >>> from bidict import BidirectionalMapping, frozenbidict + + >>> def is_immutable_bimap(obj): + ... return (isinstance(obj, BidirectionalMapping) + ... and not isinstance(obj, MutableMapping)) + + >>> is_immutable_bimap(bidict()) + False + + >>> is_immutable_bimap(frozenbidict()) True -Of course you can also use duck typing to avoid :func:`isinstance` altogether:: +Checking for ``isinstance(obj, frozenbidict)`` is too specific +and could fail in some cases. +Namely, :class:`~bidict.FrozenOrderedBidict` is an immutable mapping +but it does not subclass :class:`~bidict.frozenbidict`:: - >>> # EAFP-style: - >>> try: # doctest: +SKIP - ... foo['bar'] = 'baz' - ... except TypeError: - ... # plan B - - >>> # LBYL-style: - >>> if hasattr(foo, '__setitem__'): # doctest: +SKIP - ... foo['bar'] = 'baz' - -Also note that since -:class:`~bidict.bidict` extends -:class:`~bidict.frozenbidict`, -if you need to check whether a bidict is immutable, -testing for ``isinstance(foo, frozenbidict)`` -is not what you want:: - - >>> from bidict import frozenbidict - >>> isinstance(bidict(), frozenbidict) + >>> from bidict import FrozenOrderedBidict + >>> obj = FrozenOrderedBidict() + >>> is_immutable_bimap(obj) True + >>> isinstance(obj, frozenbidict) + False -Instead you can check for -``isinstance(foo, Hashable)`` or -``isinstance(foo, MutableMapping)`` to get the desired behavior:: +Besides the above, there are several other collections ABCs +whose interfaces are implemented by various bidict types. + +One that may be useful to know about is +:class:`collections.abc.Hashable`:: >>> from collections import Hashable >>> isinstance(frozenbidict(), Hashable) True - >>> isinstance(bidict(), Hashable) - False - >>> isinstance(bidict(), MutableMapping) + >>> isinstance(FrozenOrderedBidict(), Hashable) + True + +And although there are no ``Ordered`` or ``OrderedMapping`` ABCs, +Python 3.6 introduced the :class:`collections.abc.Reversible` ABC. +Since being reversible implies having an ordering, +you could check for reversibility +to generically detect whether a mapping is ordered:: + + >>> def is_reversible(cls): + ... try: + ... from collections import Reversible + ... except ImportError: # Python < 3.6 + ... # Better to use a shim of Python 3.6's Reversible, but this'll do for now: + ... return getattr(cls, '__reversed__', None) is not None + ... return issubclass(cls, Reversible) + + >>> def is_ordered_mapping(cls): + ... return is_reversible(cls) and issubclass(cls, Mapping) + ... + + >>> from bidict import OrderedBidict + >>> is_ordered_mapping(OrderedBidict) + True + + >>> from collections import OrderedDict + >>> is_ordered_mapping(OrderedDict) True - >>> isinstance(frozenbidict(), MutableMapping) - False diff --git a/tests/test_hypothesis.py b/tests/test_hypothesis.py index ea8e7bd..a904aa7 100644 --- a/tests/test_hypothesis.py +++ b/tests/test_hypothesis.py @@ -10,6 +10,7 @@ import gc import pickle from collections import Hashable, Mapping, MutableMapping, OrderedDict +from operator import eq, ne from os import getenv from weakref import ref @@ -112,13 +113,15 @@ HS_METHOD_ARGS = strat.sampled_from(( )) -def assert_items_match(map1, map2, assertmsg=None): +def assert_items_match(map1, map2, assertmsg=None, relation=eq): """Ensure map1 and map2 contain the same items (and in the same order, if they're ordered).""" if assertmsg is None: assertmsg = repr((map1, map2)) both_ordered = all(isinstance(m, (OrderedDict, FrozenOrderedBidict)) for m in (map1, map2)) canon = list if both_ordered else set - assert canon(iteritems(map1)) == canon(iteritems(map2)), assertmsg + canon_map1 = canon(iteritems(map1)) + canon_map2 = canon(iteritems(map2)) + assert relation(canon_map1, canon_map2), assertmsg @given(data=strat.data()) @@ -130,6 +133,8 @@ def test_eq_ne_hash(data): other_cls = data.draw(HS_MAPPING_TYPES) other_equal = other_cls(init) other_equal_inv = inv_od(iteritems(other_equal)) + assert_items_match(some_bidict, other_equal) + assert_items_match(some_bidict.inv, other_equal_inv) assert some_bidict == other_equal assert not some_bidict != other_equal assert some_bidict.inv == other_equal_inv @@ -139,7 +144,7 @@ def test_eq_ne_hash(data): if has_eq_order_sens and other_is_ordered: assert some_bidict.equals_order_sensitive(other_equal) assert some_bidict.inv.equals_order_sensitive(other_equal_inv) - both_hashable = issubclass(bi_cls, Hashable) and issubclass(other_cls, Hashable) + both_hashable = all(issubclass(cls, Hashable) for cls in (bi_cls, other_cls)) if both_hashable: assert hash(some_bidict) == hash(other_equal) @@ -147,6 +152,8 @@ def test_eq_ne_hash(data): assume(unequal_init != init) other_unequal = other_cls(unequal_init) other_unequal_inv = inv_od(iteritems(other_unequal)) + assert_items_match(some_bidict, other_unequal, relation=ne) + assert_items_match(some_bidict.inv, other_unequal_inv, relation=ne) assert some_bidict != other_unequal assert not some_bidict == other_unequal assert some_bidict.inv != other_unequal_inv @@ -169,7 +176,7 @@ def test_eq_ne_hash(data): def test_bijectivity(bi_cls, init): """*b[k] == v <==> b.inv[v] == k*""" some_bidict = bi_cls(init) - ordered = issubclass(bi_cls, FrozenOrderedBidict) + ordered = getattr(bi_cls, '__reversed__', None) canon = list if ordered else set keys = canon(iterkeys(some_bidict)) vals = canon(itervalues(some_bidict)) @@ -288,7 +295,7 @@ def test_pickle_roundtrips(bi_cls, init): some_bidict = bi_cls(init) dumps_args = {} # Pickling ordered bidicts in Python 2 requires a higher (non-default) protocol version. - if PY2 and issubclass(bi_cls, FrozenOrderedBidict): + if PY2 and issubclass(bi_cls, (OrderedBidict, FrozenOrderedBidict)): dumps_args['protocol'] = 2 pickled = pickle.dumps(some_bidict, **dumps_args) roundtripped = pickle.loads(pickled)