├── .gitignore ├── 01-data-model ├── README.md ├── data-model.ipynb ├── frenchdeck.doctest ├── frenchdeck.py ├── test.sh ├── vector2d.doctest └── vector2d.py ├── 02-array-seq ├── README.rst ├── array-seq.ipynb ├── bisect_demo.py ├── bisect_insort.py ├── lispy │ ├── py3.10 │ │ ├── examples_test.py │ │ ├── lis.py │ │ ├── lis_test.py │ │ └── quicksort.scm │ └── py3.9 │ │ ├── README.md │ │ ├── examples_test.py │ │ ├── lis.py │ │ └── lis_test.py ├── listcomp_speed.py ├── match_lat_lon.py ├── memoryviews.ipynb ├── metro_lat_lon.py └── test.sh ├── 03-dict-set ├── 03-dict-set.ipynb ├── README.md ├── dialcodes.py ├── index.py ├── index0.py ├── index_default.py ├── missing.py ├── py3.10 │ └── creator.py ├── strkeydict.py ├── strkeydict0.py ├── support │ ├── container_perftest.py │ ├── container_perftest_datagen.py │ └── hashdiff.py ├── transformdict.py └── zen.txt ├── 04-text-byte ├── .gitignore ├── README.rst ├── categories.py ├── charfinder │ ├── README.rst │ ├── cf.py │ └── test.sh ├── default_encodings.py ├── encodings-win10.txt ├── locale_sort.py ├── normeq.py ├── numerics_demo.py ├── ola.py ├── ramanujan.py ├── simplify.py ├── skin.py ├── stdout_check.py ├── two_flags.py ├── zwj_sample.ipynb ├── zwj_sample.png └── zwj_sample.py ├── 05-data-classes ├── README.asciidoc ├── cards.doctest ├── cards.py ├── cards_enum.py ├── class │ └── coordinates.py ├── dataclass │ ├── club.py │ ├── club_generic.py │ ├── club_wrong.py │ ├── coordinates.py │ ├── hackerclub.py │ ├── hackerclub_annotated.py │ ├── resource.py │ └── resource_repr.py ├── frenchdeck.doctest ├── frenchdeck.py ├── match_cities.py ├── meaning │ ├── demo_dc.py │ ├── demo_nt.py │ └── demo_plain.py └── typing_namedtuple │ ├── coordinates.py │ ├── coordinates2.py │ └── nocheck_demo.py ├── 06-obj-ref ├── README.rst ├── bus.py ├── cheese.py ├── haunted_bus.py └── twilight_bus.py ├── 07-1class-func ├── README.rst ├── bingocall.py └── tagger.py ├── 08-def-type-hints ├── README.asciidoc ├── arg_lab.py ├── birds │ ├── birds.py │ ├── daffy.py │ ├── protocol │ │ ├── lake.py │ │ ├── parrot.py │ │ └── swan.py │ └── woody.py ├── bus.py ├── callable │ └── variance.py ├── charindex.py ├── colors.py ├── columnize.py ├── columnize_test.py ├── comparable │ ├── comparable.py │ ├── top.py │ └── top_test.py ├── coordinates │ ├── coordinates.py │ ├── coordinates_named.py │ ├── coordinates_named_test.py │ ├── coordinates_test.py │ └── requirements.txt ├── ctime.py ├── double │ ├── double_object.py │ ├── double_protocol.py │ ├── double_sequence.py │ └── double_test.py ├── messages │ ├── hints_1 │ │ ├── messages.py │ │ └── messages_test.py │ ├── hints_2 │ │ ├── messages.py │ │ └── messages_test.py │ └── no_hints │ │ ├── messages.py │ │ └── messages_test.py ├── mode │ ├── mode_float.py │ └── mode_hashable.py ├── mypy.ini ├── replacer.py ├── romans.py ├── romans_test.py ├── sample.py ├── typevar_bounded.py └── typevars_constrained.py ├── 09-closure-deco ├── README.rst ├── average.py ├── average_oo.py ├── clock │ ├── clockdeco.py │ ├── clockdeco0.py │ ├── clockdeco_cls.py │ ├── clockdeco_demo.py │ ├── clockdeco_param.py │ ├── clockdeco_param_demo1.py │ └── clockdeco_param_demo2.py ├── fibo_compare.py ├── fibo_demo.py ├── fibo_demo_cache.py ├── global_x_local.rst ├── htmlizer.py ├── registration.py ├── registration_abridged.py ├── registration_param.py └── stacked.py ├── 10-dp-1class-func ├── README.rst ├── classic_strategy.py ├── classic_strategy_test.py ├── monkeytype │ ├── classic_strategy.py │ ├── classic_strategy.pyi │ ├── classic_strategy_test.py │ └── run.py ├── promotions.py ├── pytypes │ ├── classic_strategy.py │ ├── classic_strategy_test.py │ └── typelogger_output │ │ └── classic_strategy.pyi ├── requirements.txt ├── strategy.py ├── strategy_best.py ├── strategy_best2.py ├── strategy_best3.py ├── strategy_best4.py ├── strategy_param.py ├── strategy_param_test.py ├── strategy_test.py └── untyped │ ├── classic_strategy.py │ ├── promotions.py │ ├── strategy.py │ ├── strategy_best.py │ ├── strategy_best2.py │ ├── strategy_best3.py │ ├── strategy_best4.py │ ├── strategy_param.py │ └── strategy_param2.py ├── 11-pythonic-obj ├── README.md ├── mem_test.py ├── patterns.py ├── private │ ├── .gitignore │ ├── Confidential.java │ ├── Expose.java │ ├── expose.py │ ├── leakprivate.py │ └── no_respect.py ├── slots.rst ├── vector2d_v0.py ├── vector2d_v1.py ├── vector2d_v2.py ├── vector2d_v2_fmt_snippet.py ├── vector2d_v3.py ├── vector2d_v3_prophash.py └── vector2d_v3_slots.py ├── 12-seq-hacking ├── vector_v1.py ├── vector_v2.py ├── vector_v3.py ├── vector_v4.py └── vector_v5.py ├── 13-protocol-abc ├── README.rst ├── bingo.py ├── double │ ├── double_object.py │ ├── double_protocol.py │ ├── double_sequence.py │ └── double_test.py ├── drum.py ├── frenchdeck2.py ├── lotto.py ├── tombola.py ├── tombola_runner.py ├── tombola_subhook.py ├── tombola_tests.rst ├── tombolist.py └── typing │ ├── randompick.py │ ├── randompick_test.py │ ├── randompickload.py │ ├── randompickload_test.py │ ├── vector2d_v4.py │ ├── vector2d_v4_test.py │ ├── vector2d_v5.py │ └── vector2d_v5_test.py ├── 14-inheritance ├── README.rst ├── diamond.py ├── diamond2.py ├── strkeydict_dictsub.py └── uppermixin.py ├── 15-more-types ├── cafeteria │ ├── cafeteria.py │ ├── contravariant.py │ ├── covariant.py │ └── invariant.py ├── cast │ ├── empty.py │ ├── find.py │ ├── tcp_echo.py │ └── tcp_echo_no_cast.py ├── clip_annot.py ├── clip_annot_demo.py ├── clip_annot_post.py ├── collections_variance.py ├── gen_contra.py ├── lotto │ ├── generic_lotto.py │ ├── generic_lotto_demo.py │ ├── generic_lotto_errors.py │ └── tombola.py ├── mysum.py ├── petbox │ ├── petbox.py │ └── petbox_demo.py ├── protocol │ ├── abs_demo.py │ ├── mymax │ │ ├── mymax.py │ │ ├── mymax_demo.py │ │ └── mymax_test.py │ └── random │ │ ├── erp.py │ │ ├── erp_test.py │ │ ├── generic_randompick.py │ │ ├── generic_randompick_test.py │ │ ├── randompop.py │ │ └── randompop_test.py └── typeddict │ ├── books.py │ ├── books_any.py │ ├── demo_books.py │ ├── demo_not_book.py │ ├── test_books.py │ └── test_books_check_fails.py ├── 16-op-overloading ├── README.rst ├── bingo.py ├── bingoaddable.py ├── tombola.py ├── unary_plus_decimal.py ├── vector2d_v3.py ├── vector_v6.py ├── vector_v7.py └── vector_v8.py ├── 17-it-generator ├── README.rst ├── aritprog.rst ├── aritprog_float_error.py ├── aritprog_runner.py ├── aritprog_v0.py ├── aritprog_v1.py ├── aritprog_v2.py ├── aritprog_v3.py ├── columnize_iter.py ├── coroaverager.py ├── coroaverager2.py ├── fibo_by_hand.py ├── fibo_gen.py ├── isis2json │ ├── README.rst │ ├── isis2json.py │ ├── iso2709.py │ └── subfield.py ├── iter_gen_type.py ├── sentence.py ├── sentence.rst ├── sentence_gen.py ├── sentence_gen2.py ├── sentence_genexp.py ├── sentence_iter.py ├── sentence_iter2.py ├── sentence_runner.py ├── tree │ ├── 4steps │ │ ├── tree_step0.py │ │ ├── tree_step1.py │ │ ├── tree_step2.py │ │ └── tree_step3.py │ ├── classtree │ │ ├── classtree.py │ │ └── classtree_test.py │ ├── extra │ │ ├── drawtree.py │ │ ├── test_drawtree.py │ │ ├── test_tree.py │ │ └── tree.py │ ├── step0 │ │ ├── test_tree.py │ │ └── tree.py │ ├── step1 │ │ ├── test_tree.py │ │ └── tree.py │ ├── step2 │ │ ├── test_tree.py │ │ └── tree.py │ ├── step3 │ │ ├── test_tree.py │ │ └── tree.py │ ├── step4 │ │ ├── test_tree.py │ │ └── tree.py │ ├── step5 │ │ ├── test_tree.py │ │ └── tree.py │ └── step6 │ │ ├── test_tree.py │ │ └── tree.py ├── yield_delegate_fail.py └── yield_delegate_fix.py ├── 18-with-match ├── README.rst ├── lispy │ ├── LICENSE │ ├── README.md │ ├── original │ │ ├── LICENSE │ │ ├── README.md │ │ ├── lis.py │ │ ├── lispy.py │ │ └── lispytest.py │ ├── py3.10 │ │ ├── examples_test.py │ │ ├── lis.py │ │ ├── lis_test.py │ │ └── quicksort.scm │ └── py3.9 │ │ ├── README.md │ │ ├── examples_test.py │ │ ├── lis.py │ │ └── lis_test.py ├── mirror.py ├── mirror_gen.py └── mirror_gen_exc.py ├── 19-concurrency ├── primes │ ├── README.md │ ├── log-procs.txt │ ├── primes.py │ ├── procs.py │ ├── procs_race_condition.py │ ├── py36 │ │ ├── primes.py │ │ └── procs.py │ ├── run_procs.sh │ ├── sequential.py │ ├── spinner_prime_async_broken.py │ ├── spinner_prime_async_nap.py │ ├── spinner_prime_proc.py │ ├── spinner_prime_thread.py │ ├── stats-procs.ipynb │ └── threads.py ├── spinner_async.py ├── spinner_async_experiment.py ├── spinner_proc.py └── spinner_thread.py ├── 20-executors ├── demo_executor_map.py ├── getflags │ ├── .gitignore │ ├── README.adoc │ ├── country_codes.txt │ ├── flags.py │ ├── flags.zip │ ├── flags2_asyncio.py │ ├── flags2_asyncio_executor.py │ ├── flags2_common.py │ ├── flags2_sequential.py │ ├── flags2_threadpool.py │ ├── flags3_asyncio.py │ ├── flags_asyncio.py │ ├── flags_threadpool.py │ ├── flags_threadpool_futures.py │ ├── httpx-error-tree │ │ ├── drawtree.py │ │ └── tree.py │ ├── requirements.txt │ └── slow_server.py └── primes │ ├── primes.py │ └── proc_pool.py ├── 21-async ├── README.rst ├── domains │ ├── README.rst │ ├── asyncio │ │ ├── blogdom.py │ │ ├── domaincheck.py │ │ └── domainlib.py │ └── curio │ │ ├── blogdom.py │ │ ├── domaincheck.py │ │ ├── domainlib.py │ │ └── requirements.txt └── mojifinder │ ├── README.md │ ├── bottle.py │ ├── charindex.py │ ├── requirements.txt │ ├── static │ └── form.html │ ├── tcp_mojifinder.py │ ├── web_mojifinder.py │ └── web_mojifinder_bottle.py ├── 22-dyn-attr-prop ├── README.rst ├── blackknight.py ├── bulkfood │ ├── bulkfood_v1.py │ ├── bulkfood_v2.py │ ├── bulkfood_v2b.py │ └── bulkfood_v2prop.py ├── doc_property.py ├── oscon │ ├── data │ │ └── osconfeed.json │ ├── explore0.py │ ├── explore1.py │ ├── explore2.py │ ├── osconfeed-sample.json │ ├── osconfeed_explore.rst │ ├── runtests.sh │ ├── schedule_v1.py │ ├── schedule_v2.py │ ├── schedule_v3.py │ ├── schedule_v4.py │ ├── schedule_v4_hasattr.py │ ├── schedule_v5.py │ ├── test_schedule_v1.py │ ├── test_schedule_v2.py │ ├── test_schedule_v3.py │ ├── test_schedule_v4.py │ └── test_schedule_v5.py └── pseudo_construction.py ├── 23-descriptor ├── README.rst ├── bulkfood │ ├── bulkfood_v3.py │ ├── bulkfood_v4.py │ ├── bulkfood_v4c.py │ ├── bulkfood_v5.py │ ├── model_v4c.py │ └── model_v5.py ├── descriptorkinds.py ├── descriptorkinds_dump.py └── method_is_descriptor.py ├── 24-class-metaprog ├── autoconst │ ├── autoconst.py │ └── autoconst_demo.py ├── bulkfood │ ├── README.md │ ├── bulkfood_v6.py │ ├── bulkfood_v7.py │ ├── bulkfood_v8.py │ ├── model_v6.py │ ├── model_v7.py │ └── model_v8.py ├── checked │ ├── decorator │ │ ├── checkeddeco.py │ │ ├── checkeddeco_demo.py │ │ └── checkeddeco_test.py │ ├── initsub │ │ ├── checked_demo.py │ │ ├── checkedlib.py │ │ └── checkedlib_test.py │ └── metaclass │ │ ├── checked_demo.py │ │ ├── checkedlib.py │ │ └── checkedlib_test.py ├── evaltime │ ├── builderlib.py │ ├── evaldemo.py │ ├── evaldemo_meta.py │ └── metalib.py ├── factories.py ├── factories_ducktyped.py ├── hours │ ├── hours.py │ └── hours_test.py ├── metabunch │ ├── README.md │ ├── from3.6 │ │ ├── bunch.py │ │ └── bunch_test.py │ ├── nutshell3e │ │ ├── bunch.py │ │ └── bunch_test.py │ ├── original │ │ ├── bunch.py │ │ └── bunch_test.py │ └── pre3.6 │ │ ├── bunch.py │ │ └── bunch_test.py ├── persistent │ ├── .gitignore │ ├── dblib.py │ ├── dblib_test.py │ ├── persistlib.py │ └── persistlib_test.py ├── qualname │ ├── fakedjango.py │ └── models.py ├── sentinel │ ├── sentinel.py │ └── sentinel_test.py ├── setattr │ └── example_from_leo.py ├── slots │ └── slots_timing.py ├── timeslice.py └── tinyenums │ ├── microenum.py │ ├── microenum_demo.py │ ├── nanoenum.py │ └── nanoenum_demo.py ├── LICENSE ├── README.md ├── links ├── FPY.LI.htaccess └── README.md ├── pytest.ini └── ruff.toml /01-data-model/README.md: -------------------------------------------------------------------------------- 1 | # The Python Data Model 2 | 3 | Sample code for Chapter 1 of _Fluent Python 2e_ by Luciano Ramalho (O'Reilly, 2020) 4 | 5 | ## Running the tests 6 | 7 | ### Doctests 8 | 9 | Use Python's standard ``doctest`` module to check stand-alone doctest file: 10 | 11 | $ python3 -m doctest frenchdeck.doctest -v 12 | 13 | And to check doctests embedded in a module: 14 | 15 | $ python3 -m doctest vector2d.py -v 16 | 17 | ### Jupyter Notebook 18 | 19 | Install ``pytest`` and the ``nbval`` plugin: 20 | 21 | $ pip install pytest nbval 22 | 23 | Run: 24 | 25 | $ pytest --nbval 26 | -------------------------------------------------------------------------------- /01-data-model/frenchdeck.py: -------------------------------------------------------------------------------- 1 | import collections 2 | 3 | Card = collections.namedtuple('Card', ['rank', 'suit']) 4 | 5 | class FrenchDeck: 6 | ranks = [str(n) for n in range(2, 11)] + list('JQKA') 7 | suits = 'spades diamonds clubs hearts'.split() 8 | 9 | def __init__(self): 10 | self._cards = [Card(rank, suit) for suit in self.suits 11 | for rank in self.ranks] 12 | 13 | def __len__(self): 14 | return len(self._cards) 15 | 16 | def __getitem__(self, position): 17 | return self._cards[position] 18 | -------------------------------------------------------------------------------- /01-data-model/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | python3 -m doctest frenchdeck.doctest 3 | python3 -m doctest vector2d.py 4 | pytest -q --nbval 5 | -------------------------------------------------------------------------------- /01-data-model/vector2d.doctest: -------------------------------------------------------------------------------- 1 | >>> from vector2d import Vector 2 | >>> v1 = Vector(2, 4) 3 | >>> v2 = Vector(2, 1) 4 | >>> v1 + v2 5 | Vector(4, 5) 6 | >>> v = Vector(3, 4) 7 | >>> abs(v) 8 | 5.0 9 | >>> v * 3 10 | Vector(9, 12) 11 | >>> abs(v * 3) 12 | 15.0 13 | -------------------------------------------------------------------------------- /01-data-model/vector2d.py: -------------------------------------------------------------------------------- 1 | """ 2 | vector2d.py: a simplistic class demonstrating some special methods 3 | 4 | It is simplistic for didactic reasons. It lacks proper error handling, 5 | especially in the ``__add__`` and ``__mul__`` methods. 6 | 7 | This example is greatly expanded later in the book. 8 | 9 | Addition:: 10 | 11 | >>> v1 = Vector(2, 4) 12 | >>> v2 = Vector(2, 1) 13 | >>> v1 + v2 14 | Vector(4, 5) 15 | 16 | Absolute value:: 17 | 18 | >>> v = Vector(3, 4) 19 | >>> abs(v) 20 | 5.0 21 | 22 | Scalar multiplication:: 23 | 24 | >>> v * 3 25 | Vector(9, 12) 26 | >>> abs(v * 3) 27 | 15.0 28 | 29 | """ 30 | 31 | 32 | import math 33 | 34 | class Vector: 35 | 36 | def __init__(self, x=0, y=0): 37 | self.x = x 38 | self.y = y 39 | 40 | def __repr__(self): 41 | return f'Vector({self.x!r}, {self.y!r})' 42 | 43 | def __abs__(self): 44 | return math.hypot(self.x, self.y) 45 | 46 | def __bool__(self): 47 | return bool(abs(self)) 48 | 49 | def __add__(self, other): 50 | x = self.x + other.x 51 | y = self.y + other.y 52 | return Vector(x, y) 53 | 54 | def __mul__(self, scalar): 55 | return Vector(self.x * scalar, self.y * scalar) 56 | -------------------------------------------------------------------------------- /02-array-seq/README.rst: -------------------------------------------------------------------------------- 1 | Sample code for Chapter 2 - "An array of sequences" 2 | 3 | From the book "Fluent Python 2e" by Luciano Ramalho (O'Reilly) 4 | http://shop.oreilly.com/product/0636920032519.do 5 | -------------------------------------------------------------------------------- /02-array-seq/bisect_insort.py: -------------------------------------------------------------------------------- 1 | import bisect 2 | import random 3 | 4 | SIZE = 7 5 | 6 | random.seed(1729) 7 | 8 | my_list = [] 9 | for i in range(SIZE): 10 | new_item = random.randrange(SIZE * 2) 11 | bisect.insort(my_list, new_item) 12 | print(f'{new_item:2d} -> {my_list}') 13 | -------------------------------------------------------------------------------- /02-array-seq/lispy/py3.10/quicksort.scm: -------------------------------------------------------------------------------- 1 | (define (quicksort lst) 2 | (if (null? lst) 3 | lst 4 | (begin 5 | (define pivot (car lst)) 6 | (define rest (cdr lst)) 7 | (append 8 | (quicksort 9 | (filter (lambda (x) (< x pivot)) rest)) 10 | (list pivot) 11 | (quicksort 12 | (filter (lambda (x) (>= x pivot)) rest))) 13 | ) 14 | ) 15 | ) 16 | (display 17 | (quicksort (list 2 1 6 3 4 0 8 9 7 5))) 18 | -------------------------------------------------------------------------------- /02-array-seq/listcomp_speed.py: -------------------------------------------------------------------------------- 1 | import timeit 2 | 3 | TIMES = 10000 4 | 5 | SETUP = """ 6 | symbols = '$¢£¥€¤' 7 | def non_ascii(c): 8 | return c > 127 9 | """ 10 | 11 | def clock(label, cmd): 12 | res = timeit.repeat(cmd, setup=SETUP, number=TIMES) 13 | print(label, *(f'{x:.3f}' for x in res)) 14 | 15 | clock('listcomp :', '[ord(s) for s in symbols if ord(s) > 127]') 16 | clock('listcomp + func :', '[ord(s) for s in symbols if non_ascii(ord(s))]') 17 | clock('filter + lambda :', 'list(filter(lambda c: c > 127, map(ord, symbols)))') 18 | clock('filter + func :', 'list(filter(non_ascii, map(ord, symbols)))') 19 | -------------------------------------------------------------------------------- /02-array-seq/match_lat_lon.py: -------------------------------------------------------------------------------- 1 | """ 2 | metro_lat_long.py 3 | 4 | Demonstration of nested tuple unpacking:: 5 | 6 | >>> main() 7 | | latitude | longitude 8 | Mexico City | 19.4333 | -99.1333 9 | New York-Newark | 40.8086 | -74.0204 10 | São Paulo | -23.5478 | -46.6358 11 | 12 | """ 13 | 14 | # tag::MAIN[] 15 | metro_areas = [ 16 | ('Tokyo', 'JP', 36.933, (35.689722, 139.691667)), 17 | ('Delhi NCR', 'IN', 21.935, (28.613889, 77.208889)), 18 | ('Mexico City', 'MX', 20.142, (19.433333, -99.133333)), 19 | ('New York-Newark', 'US', 20.104, (40.808611, -74.020386)), 20 | ('São Paulo', 'BR', 19.649, (-23.547778, -46.635833)), 21 | ] 22 | 23 | def main(): 24 | print(f'{"":15} | {"latitude":>9} | {"longitude":>9}') 25 | for record in metro_areas: 26 | match record: # <1> 27 | case [name, _, _, (lat, lon)] if lon <= 0: # <2> 28 | print(f'{name:15} | {lat:9.4f} | {lon:9.4f}') 29 | # end::MAIN[] 30 | 31 | if __name__ == '__main__': 32 | main() 33 | -------------------------------------------------------------------------------- /02-array-seq/metro_lat_lon.py: -------------------------------------------------------------------------------- 1 | """ 2 | metro_lat_lon.py 3 | 4 | Demonstration of nested tuple unpacking:: 5 | 6 | >>> main() 7 | | latitude | longitude 8 | Mexico City | 19.4333 | -99.1333 9 | New York-Newark | 40.8086 | -74.0204 10 | São Paulo | -23.5478 | -46.6358 11 | 12 | """ 13 | 14 | # tag::MAIN[] 15 | metro_areas = [ 16 | ('Tokyo', 'JP', 36.933, (35.689722, 139.691667)), # <1> 17 | ('Delhi NCR', 'IN', 21.935, (28.613889, 77.208889)), 18 | ('Mexico City', 'MX', 20.142, (19.433333, -99.133333)), 19 | ('New York-Newark', 'US', 20.104, (40.808611, -74.020386)), 20 | ('São Paulo', 'BR', 19.649, (-23.547778, -46.635833)), 21 | ] 22 | 23 | def main(): 24 | print(f'{"":15} | {"latitude":>9} | {"longitude":>9}') 25 | for name, _, _, (lat, lon) in metro_areas: # <2> 26 | if lon <= 0: # <3> 27 | print(f'{name:15} | {lat:9.4f} | {lon:9.4f}') 28 | 29 | if __name__ == '__main__': 30 | main() 31 | # end::MAIN[] -------------------------------------------------------------------------------- /02-array-seq/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | python3 -m doctest bisect_demo.py 3 | python3 -m doctest metro_lat_lon.py 4 | pytest -q --nbval 5 | -------------------------------------------------------------------------------- /03-dict-set/README.md: -------------------------------------------------------------------------------- 1 | # Dictionaries and Sets 2 | 3 | Sample code for Chapter 3 of _Fluent Python 2e_ by Luciano Ramalho (O'Reilly, 2020) 4 | 5 | ## Running the tests 6 | 7 | ### Doctests 8 | 9 | Use Python's standard ``doctest`` module, for example: 10 | 11 | $ python3 -m doctest bisect_demo.py -v 12 | 13 | ### Jupyter Notebook 14 | 15 | Install ``pytest`` and the ``nbval`` plugin: 16 | 17 | $ pip install pytest nbval 18 | 19 | Run: 20 | 21 | $ pytest --nbval -------------------------------------------------------------------------------- /03-dict-set/dialcodes.py: -------------------------------------------------------------------------------- 1 | # tag::DIALCODES[] 2 | # dial codes of the top 10 most populous countries 3 | DIAL_CODES = [ 4 | (86, 'China'), 5 | (91, 'India'), 6 | (1, 'United States'), 7 | (62, 'Indonesia'), 8 | (55, 'Brazil'), 9 | (92, 'Pakistan'), 10 | (880, 'Bangladesh'), 11 | (234, 'Nigeria'), 12 | (7, 'Russia'), 13 | (81, 'Japan'), 14 | ] 15 | 16 | d1 = dict(DIAL_CODES) # <1> 17 | print('d1:', d1.keys()) 18 | d2 = dict(sorted(DIAL_CODES)) # <2> 19 | print('d2:', d2.keys()) 20 | d3 = dict(sorted(DIAL_CODES, key=lambda x: x[1])) # <3> 21 | print('d3:', d3.keys()) 22 | assert d1 == d2 and d2 == d3 # <4> 23 | # end::DIALCODES[] 24 | """ 25 | # tag::DIALCODES_OUTPUT[] 26 | d1: dict_keys([880, 1, 86, 55, 7, 234, 91, 92, 62, 81]) 27 | d2: dict_keys([880, 1, 91, 86, 81, 55, 234, 7, 92, 62]) 28 | d3: dict_keys([880, 81, 1, 86, 55, 7, 234, 91, 92, 62]) 29 | # end::DIALCODES_OUTPUT[] 30 | """ 31 | -------------------------------------------------------------------------------- /03-dict-set/index.py: -------------------------------------------------------------------------------- 1 | # adapted from Alex Martelli's example in "Re-learning Python" 2 | # http://www.aleax.it/Python/accu04_Relearn_Python_alex.pdf 3 | # (slide 41) Ex: lines-by-word file index 4 | 5 | # tag::INDEX[] 6 | """Build an index mapping word -> list of occurrences""" 7 | 8 | import re 9 | import sys 10 | 11 | WORD_RE = re.compile(r'\w+') 12 | 13 | index = {} 14 | with open(sys.argv[1], encoding='utf-8') as fp: 15 | for line_no, line in enumerate(fp, 1): 16 | for match in WORD_RE.finditer(line): 17 | word = match.group() 18 | column_no = match.start() + 1 19 | location = (line_no, column_no) 20 | index.setdefault(word, []).append(location) # <1> 21 | 22 | # display in alphabetical order 23 | for word in sorted(index, key=str.upper): 24 | print(word, index[word]) 25 | # end::INDEX[] 26 | -------------------------------------------------------------------------------- /03-dict-set/index0.py: -------------------------------------------------------------------------------- 1 | # adapted from Alex Martelli's example in "Re-learning Python" 2 | # http://www.aleax.it/Python/accu04_Relearn_Python_alex.pdf 3 | # (slide 41) Ex: lines-by-word file index 4 | 5 | # tag::INDEX0[] 6 | """Build an index mapping word -> list of occurrences""" 7 | 8 | import re 9 | import sys 10 | 11 | WORD_RE = re.compile(r'\w+') 12 | 13 | index = {} 14 | with open(sys.argv[1], encoding='utf-8') as fp: 15 | for line_no, line in enumerate(fp, 1): 16 | for match in WORD_RE.finditer(line): 17 | word = match.group() 18 | column_no = match.start() + 1 19 | location = (line_no, column_no) 20 | # this is ugly; coded like this to make a point 21 | occurrences = index.get(word, []) # <1> 22 | occurrences.append(location) # <2> 23 | index[word] = occurrences # <3> 24 | 25 | # display in alphabetical order 26 | for word in sorted(index, key=str.upper): # <4> 27 | print(word, index[word]) 28 | # end::INDEX0[] 29 | -------------------------------------------------------------------------------- /03-dict-set/index_default.py: -------------------------------------------------------------------------------- 1 | # adapted from Alex Martelli's example in "Re-learning Python" 2 | # http://www.aleax.it/Python/accu04_Relearn_Python_alex.pdf 3 | # (slide 41) Ex: lines-by-word file index 4 | 5 | # tag::INDEX_DEFAULT[] 6 | """Build an index mapping word -> list of occurrences""" 7 | 8 | import collections 9 | import re 10 | import sys 11 | 12 | WORD_RE = re.compile(r'\w+') 13 | 14 | index = collections.defaultdict(list) # <1> 15 | with open(sys.argv[1], encoding='utf-8') as fp: 16 | for line_no, line in enumerate(fp, 1): 17 | for match in WORD_RE.finditer(line): 18 | word = match.group() 19 | column_no = match.start() + 1 20 | location = (line_no, column_no) 21 | index[word].append(location) # <2> 22 | 23 | # display in alphabetical order 24 | for word in sorted(index, key=str.upper): 25 | print(word, index[word]) 26 | # end::INDEX_DEFAULT[] 27 | -------------------------------------------------------------------------------- /03-dict-set/strkeydict0.py: -------------------------------------------------------------------------------- 1 | """StrKeyDict0 converts non-string keys to `str` on lookup 2 | 3 | # tag::STRKEYDICT0_TESTS[] 4 | 5 | Tests for item retrieval using `d[key]` notation:: 6 | 7 | >>> d = StrKeyDict0([('2', 'two'), ('4', 'four')]) 8 | >>> d['2'] 9 | 'two' 10 | >>> d[4] 11 | 'four' 12 | >>> d[1] 13 | Traceback (most recent call last): 14 | ... 15 | KeyError: '1' 16 | 17 | Tests for item retrieval using `d.get(key)` notation:: 18 | 19 | >>> d.get('2') 20 | 'two' 21 | >>> d.get(4) 22 | 'four' 23 | >>> d.get(1, 'N/A') 24 | 'N/A' 25 | 26 | 27 | Tests for the `in` operator:: 28 | 29 | >>> 2 in d 30 | True 31 | >>> 1 in d 32 | False 33 | 34 | # end::STRKEYDICT0_TESTS[] 35 | """ 36 | 37 | 38 | # tag::STRKEYDICT0[] 39 | class StrKeyDict0(dict): # <1> 40 | 41 | def __missing__(self, key): 42 | if isinstance(key, str): # <2> 43 | raise KeyError(key) 44 | return self[str(key)] # <3> 45 | 46 | def get(self, key, default=None): 47 | try: 48 | return self[key] # <4> 49 | except KeyError: 50 | return default # <5> 51 | 52 | def __contains__(self, key): 53 | return key in self.keys() or str(key) in self.keys() # <6> 54 | 55 | # end::STRKEYDICT0[] 56 | -------------------------------------------------------------------------------- /03-dict-set/support/container_perftest_datagen.py: -------------------------------------------------------------------------------- 1 | """ 2 | Generate data for container performance test 3 | """ 4 | 5 | import array 6 | import random 7 | 8 | MAX_EXPONENT = 7 9 | HAYSTACK_LEN = 10 ** MAX_EXPONENT 10 | NEEDLES_LEN = 10 ** (MAX_EXPONENT - 1) 11 | SAMPLE_LEN = HAYSTACK_LEN + NEEDLES_LEN // 2 12 | 13 | needles = array.array('d') 14 | 15 | sample = {1 / random.random() for i in range(SAMPLE_LEN)} 16 | print(f'initial sample: {len(sample)} elements') 17 | 18 | # complete sample, in case duplicate random numbers were discarded 19 | while len(sample) < SAMPLE_LEN: 20 | sample.add(1 / random.random()) 21 | 22 | print(f'complete sample: {len(sample)} elements') 23 | 24 | sample = array.array('d', sample) 25 | random.shuffle(sample) 26 | 27 | not_selected = sample[:NEEDLES_LEN // 2] 28 | print(f'not selected: {len(not_selected)} samples') 29 | print(' writing not_selected.arr') 30 | with open('not_selected.arr', 'wb') as fp: 31 | not_selected.tofile(fp) 32 | 33 | selected = sample[NEEDLES_LEN // 2:] 34 | print(f'selected: {len(selected)} samples') 35 | print(' writing selected.arr') 36 | with open('selected.arr', 'wb') as fp: 37 | selected.tofile(fp) 38 | -------------------------------------------------------------------------------- /03-dict-set/support/hashdiff.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | MAX_BITS = len(format(sys.maxsize, 'b')) 4 | print(f'{MAX_BITS + 1}-bit Python build') 5 | 6 | def hash_diff(o1, o2): 7 | h1 = f'{hash(o1):>0{MAX_BITS}b}' 8 | h2 = f'{hash(o2):>0{MAX_BITS}b}' 9 | diff = ''.join('!' if b1 != b2 else ' ' for b1, b2 in zip(h1, h2)) 10 | count = f'!= {diff.count("!")}' 11 | width = max(len(repr(o1)), len(repr(o2)), 8) 12 | sep = '-' * (width * 2 + MAX_BITS) 13 | return (f'{o1!r:{width}} {h1}\n{" ":{width}} {diff} {count}\n' 14 | f'{o2!r:{width}} {h2}\n{sep}') 15 | 16 | if __name__ == '__main__': 17 | print(hash_diff(1, 1.0)) 18 | print(hash_diff(1.0, 1.0001)) 19 | print(hash_diff(1.0001, 1.0002)) 20 | print(hash_diff(1.0002, 1.0003)) 21 | -------------------------------------------------------------------------------- /03-dict-set/zen.txt: -------------------------------------------------------------------------------- 1 | The Zen of Python, by Tim Peters 2 | 3 | Beautiful is better than ugly. 4 | Explicit is better than implicit. 5 | Simple is better than complex. 6 | Complex is better than complicated. 7 | Flat is better than nested. 8 | Sparse is better than dense. 9 | Readability counts. 10 | Special cases aren't special enough to break the rules. 11 | Although practicality beats purity. 12 | Errors should never pass silently. 13 | Unless explicitly silenced. 14 | In the face of ambiguity, refuse the temptation to guess. 15 | There should be one-- and preferably only one --obvious way to do it. 16 | Although that way may not be obvious at first unless you're Dutch. 17 | Now is better than never. 18 | Although never is often better than *right* now. 19 | If the implementation is hard to explain, it's a bad idea. 20 | If the implementation is easy to explain, it may be a good idea. 21 | Namespaces are one honking great idea -- let's do more of those! 22 | -------------------------------------------------------------------------------- /04-text-byte/.gitignore: -------------------------------------------------------------------------------- 1 | dummy 2 | -------------------------------------------------------------------------------- /04-text-byte/README.rst: -------------------------------------------------------------------------------- 1 | Sample code for Chapter 4 - "Text and bytes" 2 | 3 | From the book "Fluent Python" by Luciano Ramalho (O'Reilly, 2015) 4 | http://shop.oreilly.com/product/0636920032519.do 5 | -------------------------------------------------------------------------------- /04-text-byte/categories.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import collections 3 | from unicodedata import category 4 | 5 | 6 | def category_stats(): 7 | counts = collections.Counter() 8 | firsts = {} 9 | for code in range(sys.maxunicode + 1): 10 | char = chr(code) 11 | cat = category(char) 12 | if cat not in counts: 13 | firsts[cat] = char 14 | counts[cat] += 1 15 | return counts, firsts 16 | 17 | 18 | def category_scan(desired): 19 | for code in range(sys.maxunicode + 1): 20 | char = chr(code) 21 | if category(char) == desired: 22 | yield char 23 | 24 | 25 | def main(args): 26 | count = 0 27 | if len(args) == 2: 28 | for char in category_scan(args[1]): 29 | print(char, end=' ') 30 | count += 1 31 | if count > 200: 32 | break 33 | print() 34 | print(count, 'characters shown') 35 | else: 36 | counts, firsts = category_stats() 37 | for i, (cat, count) in enumerate(counts.most_common(), 1): 38 | first = firsts[cat] 39 | if cat == 'Cs': 40 | first = f'(surrogate U+{ord(first):04X})' 41 | print(f'{i:2} {count:6} {cat} {first}') 42 | 43 | 44 | if __name__ == '__main__': 45 | main(sys.argv) 46 | -------------------------------------------------------------------------------- /04-text-byte/charfinder/cf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import sys 3 | import unicodedata 4 | 5 | START, END = ord(' '), sys.maxunicode + 1 # <1> 6 | 7 | def find(*query_words, start=START, end=END): # <2> 8 | query = {w.upper() for w in query_words} # <3> 9 | for code in range(start, end): 10 | char = chr(code) # <4> 11 | name = unicodedata.name(char, None) # <5> 12 | if name and query.issubset(name.split()): # <6> 13 | print(f'U+{code:04X}\t{char}\t{name}') # <7> 14 | 15 | def main(words): 16 | if words: 17 | find(*words) 18 | else: 19 | print('Please provide words to find.') 20 | 21 | if __name__ == '__main__': 22 | main(sys.argv[1:]) 23 | -------------------------------------------------------------------------------- /04-text-byte/charfinder/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | python3 -m doctest README.rst $1 3 | -------------------------------------------------------------------------------- /04-text-byte/default_encodings.py: -------------------------------------------------------------------------------- 1 | import locale 2 | import sys 3 | 4 | expressions = """ 5 | locale.getpreferredencoding() 6 | type(my_file) 7 | my_file.encoding 8 | sys.stdout.isatty() 9 | sys.stdout.encoding 10 | sys.stdin.isatty() 11 | sys.stdin.encoding 12 | sys.stderr.isatty() 13 | sys.stderr.encoding 14 | sys.getdefaultencoding() 15 | sys.getfilesystemencoding() 16 | """ 17 | 18 | my_file = open('dummy', 'w') 19 | 20 | for expression in expressions.split(): 21 | value = eval(expression) 22 | print(f'{expression:>30} -> {value!r}') 23 | -------------------------------------------------------------------------------- /04-text-byte/encodings-win10.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluentpython/example-code-2e/cf3161ca006b106bfc0ba698e9135e9cdfb51e55/04-text-byte/encodings-win10.txt -------------------------------------------------------------------------------- /04-text-byte/locale_sort.py: -------------------------------------------------------------------------------- 1 | import locale 2 | my_locale = locale.setlocale(locale.LC_COLLATE, 'pt_BR.UTF-8') 3 | print(my_locale) 4 | fruits = ['caju', 'atemoia', 'cajá', 'açaí', 'acerola'] 5 | sorted_fruits = sorted(fruits, key=locale.strxfrm) 6 | print(sorted_fruits) 7 | -------------------------------------------------------------------------------- /04-text-byte/normeq.py: -------------------------------------------------------------------------------- 1 | """ 2 | Utility functions for normalized Unicode string comparison. 3 | 4 | Using Normal Form C, case sensitive: 5 | 6 | >>> s1 = 'café' 7 | >>> s2 = 'cafe\u0301' 8 | >>> s1 == s2 9 | False 10 | >>> nfc_equal(s1, s2) 11 | True 12 | >>> nfc_equal('A', 'a') 13 | False 14 | 15 | Using Normal Form C with case folding: 16 | 17 | >>> s3 = 'Straße' 18 | >>> s4 = 'strasse' 19 | >>> s3 == s4 20 | False 21 | >>> nfc_equal(s3, s4) 22 | False 23 | >>> fold_equal(s3, s4) 24 | True 25 | >>> fold_equal(s1, s2) 26 | True 27 | >>> fold_equal('A', 'a') 28 | True 29 | 30 | """ 31 | 32 | from unicodedata import normalize 33 | 34 | def nfc_equal(str1, str2): 35 | return normalize('NFC', str1) == normalize('NFC', str2) 36 | 37 | def fold_equal(str1, str2): 38 | return (normalize('NFC', str1).casefold() == 39 | normalize('NFC', str2).casefold()) 40 | -------------------------------------------------------------------------------- /04-text-byte/numerics_demo.py: -------------------------------------------------------------------------------- 1 | # tag::NUMERICS_DEMO[] 2 | import unicodedata 3 | import re 4 | 5 | re_digit = re.compile(r'\d') 6 | 7 | sample = '1\xbc\xb2\u0969\u136b\u216b\u2466\u2480\u3285' 8 | 9 | for char in sample: 10 | print(f'U+{ord(char):04x}', # <1> 11 | char.center(6), # <2> 12 | 're_dig' if re_digit.match(char) else '-', # <3> 13 | 'isdig' if char.isdigit() else '-', # <4> 14 | 'isnum' if char.isnumeric() else '-', # <5> 15 | f'{unicodedata.numeric(char):5.2f}', # <6> 16 | unicodedata.name(char), # <7> 17 | sep='\t') 18 | # end::NUMERICS_DEMO[] 19 | -------------------------------------------------------------------------------- /04-text-byte/ola.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluentpython/example-code-2e/cf3161ca006b106bfc0ba698e9135e9cdfb51e55/04-text-byte/ola.py -------------------------------------------------------------------------------- /04-text-byte/ramanujan.py: -------------------------------------------------------------------------------- 1 | # tag::RE_DEMO[] 2 | import re 3 | 4 | re_numbers_str = re.compile(r'\d+') # <1> 5 | re_words_str = re.compile(r'\w+') 6 | re_numbers_bytes = re.compile(rb'\d+') # <2> 7 | re_words_bytes = re.compile(rb'\w+') 8 | 9 | text_str = ("Ramanujan saw \u0be7\u0bed\u0be8\u0bef" # <3> 10 | " as 1729 = 1³ + 12³ = 9³ + 10³.") # <4> 11 | 12 | text_bytes = text_str.encode('utf_8') # <5> 13 | 14 | print(f'Text\n {text_str!r}') 15 | print('Numbers') 16 | print(' str :', re_numbers_str.findall(text_str)) # <6> 17 | print(' bytes:', re_numbers_bytes.findall(text_bytes)) # <7> 18 | print('Words') 19 | print(' str :', re_words_str.findall(text_str)) # <8> 20 | print(' bytes:', re_words_bytes.findall(text_bytes)) # <9> 21 | # end::RE_DEMO[] 22 | -------------------------------------------------------------------------------- /04-text-byte/skin.py: -------------------------------------------------------------------------------- 1 | from unicodedata import name 2 | 3 | SKIN1 = 0x1F3FB # EMOJI MODIFIER FITZPATRICK TYPE-1-2 # <1> 4 | SKINS = [chr(i) for i in range(SKIN1, SKIN1 + 5)] # <2> 5 | THUMB = '\U0001F44d' # THUMBS UP SIGN 👍 6 | 7 | examples = [THUMB] # <3> 8 | examples.extend(THUMB + skin for skin in SKINS) # <4> 9 | 10 | for example in examples: 11 | print(example, end='\t') # <5> 12 | print(' + '.join(name(char) for char in example)) # <6> 13 | -------------------------------------------------------------------------------- /04-text-byte/stdout_check.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from unicodedata import name 3 | 4 | print(sys.version) 5 | print() 6 | print('sys.stdout.isatty():', sys.stdout.isatty()) 7 | print('sys.stdout.encoding:', sys.stdout.encoding) 8 | print() 9 | 10 | test_chars = [ 11 | '\N{HORIZONTAL ELLIPSIS}', # exists in cp1252, not in cp437 12 | '\N{INFINITY}', # exists in cp437, not in cp1252 13 | '\N{CIRCLED NUMBER FORTY TWO}', # not in cp437 or in cp1252 14 | ] 15 | 16 | for char in test_chars: 17 | print(f'Trying to output {name(char)}:') 18 | print(char) 19 | -------------------------------------------------------------------------------- /04-text-byte/two_flags.py: -------------------------------------------------------------------------------- 1 | # REGIONAL INDICATOR SYMBOLS 2 | RIS_A = '\U0001F1E6' # LETTER A 3 | RIS_U = '\U0001F1FA' # LETTER U 4 | print(RIS_A + RIS_U) # AU: Australia 5 | print(RIS_U + RIS_A) # UA: Ukraine 6 | print(RIS_A + RIS_A) # AA: no such country 7 | -------------------------------------------------------------------------------- /04-text-byte/zwj_sample.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluentpython/example-code-2e/cf3161ca006b106bfc0ba698e9135e9cdfb51e55/04-text-byte/zwj_sample.png -------------------------------------------------------------------------------- /04-text-byte/zwj_sample.py: -------------------------------------------------------------------------------- 1 | from unicodedata import name 2 | 3 | zwg_sample = """ 4 | 1F468 200D 1F9B0 |man: red hair |E11.0 5 | 1F9D1 200D 1F91D 200D 1F9D1 |people holding hands |E12.0 6 | 1F3CA 1F3FF 200D 2640 FE0F |woman swimming: dark skin tone |E4.0 7 | 1F469 1F3FE 200D 2708 FE0F |woman pilot: medium-dark skin tone |E4.0 8 | 1F468 200D 1F469 200D 1F467 |family: man, woman, girl |E2.0 9 | 1F3F3 FE0F 200D 26A7 FE0F |transgender flag |E13.0 10 | 1F469 200D 2764 FE0F 200D 1F48B 200D 1F469 |kiss: woman, woman |E2.0 11 | """ 12 | 13 | markers = {'\u200D': 'ZWG', # ZERO WIDTH JOINER 14 | '\uFE0F': 'V16', # VARIATION SELECTOR-16 15 | } 16 | 17 | for line in zwg_sample.strip().split('\n'): 18 | code, descr, version = (s.strip() for s in line.split('|')) 19 | chars = [chr(int(c, 16)) for c in code.split()] 20 | print(''.join(chars), version, descr, sep='\t', end='') 21 | for char in chars: 22 | if char in markers: 23 | print(' + ' + markers[char], end='') 24 | else: 25 | ucode = f'U+{ord(char):04X}' 26 | print(f'\n\t{char}\t{ucode}\t{name(char)}', end='') 27 | print() 28 | -------------------------------------------------------------------------------- /05-data-classes/README.asciidoc: -------------------------------------------------------------------------------- 1 | == Record-like Structures 2 | -------------------------------------------------------------------------------- /05-data-classes/cards.doctest: -------------------------------------------------------------------------------- 1 | >>> from cards import Card 2 | >>> helen = Card('Q', 'hearts') 3 | >>> helen 4 | Card(rank='Q', suit='hearts') 5 | 6 | >>> cards = [Card(r, s) for s in Card.suits for r in Card.ranks] 7 | >>> cards[:3] 8 | [Card(rank='2', suit='spades'), Card(rank='3', suit='spades'), Card(rank='4', suit='spades')] 9 | >>> sorted(cards)[:3] 10 | [Card(rank='2', suit='clubs'), Card(rank='2', suit='diamonds'), Card(rank='2', suit='hearts')] 11 | 12 | >>> from cards_enum import Card, Suit, Rank 13 | >>> helen = Card('Q', 'hearts') 14 | >>> helen 15 | Card(rank='Q', suit='hearts') 16 | 17 | >>> cards = [Card(r, s) for s in Suit for r in Rank] 18 | >>> cards[:3] 19 | [Card(rank='2', suit='spades'), Card(rank='3', suit='spades'), Card(rank='4', suit='spades')] 20 | >>> sorted(cards)[:3] 21 | [Card(rank='2', suit='clubs'), Card(rank='2', suit='diamonds'), Card(rank='2', suit='hearts')] 22 | >>> for card in cards[12::13]: print(card) 23 | -------------------------------------------------------------------------------- /05-data-classes/cards.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | @dataclass(order=True) 4 | class Card: 5 | rank: str 6 | suit: str 7 | 8 | ranks = [str(n) for n in range(2, 10)] + list('JQKA') 9 | suits = 'spades diamonds clubs hearts'.split() 10 | -------------------------------------------------------------------------------- /05-data-classes/cards_enum.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | import enum 3 | 4 | Suit = enum.IntEnum('Suit', 'spades diamonds clubs hearts') 5 | Rank = enum.Enum('Rank', [str(n) for n in range(2, 10)] + list('JQKA')) 6 | 7 | @dataclass(order=True) 8 | class Card: 9 | rank: Suit 10 | suit: Rank 11 | 12 | def __str__(self): 13 | glyphs = [chr(x) for x in range(0x2660, 0x2664)] 14 | return f'{self.rank} of {glyphs[self.suit-1]}' 15 | -------------------------------------------------------------------------------- /05-data-classes/class/coordinates.py: -------------------------------------------------------------------------------- 1 | """ 2 | ``Coordinate``: a simple class with a custom ``__str__``:: 3 | 4 | >>> moscow = Coordinate(55.756, 37.617) 5 | >>> print(moscow) # doctest:+ELLIPSIS 6 | 7 | """ 8 | 9 | # tag::COORDINATE[] 10 | class Coordinate: 11 | 12 | def __init__(self, lat, lon): 13 | self.lat = lat 14 | self.lon = lon 15 | 16 | # end::COORDINATE[] -------------------------------------------------------------------------------- /05-data-classes/dataclass/club.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass, field 2 | 3 | @dataclass 4 | class ClubMember: 5 | name: str 6 | guests: list = field(default_factory=list) 7 | 8 | -------------------------------------------------------------------------------- /05-data-classes/dataclass/club_generic.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass, field 2 | 3 | @dataclass 4 | class ClubMember: 5 | name: str 6 | guests: list[str] = field(default_factory=list) # <1> 7 | -------------------------------------------------------------------------------- /05-data-classes/dataclass/club_wrong.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | # tag::CLUBMEMBER[] 4 | @dataclass 5 | class ClubMember: 6 | name: str 7 | guests: list = [] 8 | # end::CLUBMEMBER[] 9 | -------------------------------------------------------------------------------- /05-data-classes/dataclass/coordinates.py: -------------------------------------------------------------------------------- 1 | """ 2 | ``Coordinate``: simple class decorated with ``dataclass`` and a custom ``__str__``:: 3 | 4 | >>> moscow = Coordinate(55.756, 37.617) 5 | >>> print(moscow) 6 | 55.8°N, 37.6°E 7 | 8 | """ 9 | 10 | # tag::COORDINATE[] 11 | 12 | from dataclasses import dataclass 13 | 14 | @dataclass(frozen=True) 15 | class Coordinate: 16 | lat: float 17 | lon: float 18 | 19 | def __str__(self): 20 | ns = 'N' if self.lat >= 0 else 'S' 21 | we = 'E' if self.lon >= 0 else 'W' 22 | return f'{abs(self.lat):.1f}°{ns}, {abs(self.lon):.1f}°{we}' 23 | # end::COORDINATE[] 24 | -------------------------------------------------------------------------------- /05-data-classes/frenchdeck.py: -------------------------------------------------------------------------------- 1 | import collections 2 | 3 | Card = collections.namedtuple('Card', ['rank', 'suit']) 4 | 5 | class FrenchDeck: 6 | ranks = [str(n) for n in range(2, 11)] + list('JQKA') 7 | suits = 'spades diamonds clubs hearts'.split() 8 | 9 | def __init__(self): 10 | self._cards = [Card(rank, suit) for suit in self.suits 11 | for rank in self.ranks] 12 | 13 | def __len__(self): 14 | return len(self._cards) 15 | 16 | def __getitem__(self, position): 17 | return self._cards[position] 18 | -------------------------------------------------------------------------------- /05-data-classes/meaning/demo_dc.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | @dataclass 4 | class DemoDataClass: 5 | a: int # <1> 6 | b: float = 1.1 # <2> 7 | c = 'spam' # <3> 8 | -------------------------------------------------------------------------------- /05-data-classes/meaning/demo_nt.py: -------------------------------------------------------------------------------- 1 | import typing 2 | 3 | class DemoNTClass(typing.NamedTuple): 4 | a: int # <1> 5 | b: float = 1.1 # <2> 6 | c = 'spam' # <3> 7 | -------------------------------------------------------------------------------- /05-data-classes/meaning/demo_plain.py: -------------------------------------------------------------------------------- 1 | class DemoPlainClass: 2 | a: int # <1> 3 | b: float = 1.1 # <2> 4 | c = 'spam' # <3> 5 | -------------------------------------------------------------------------------- /05-data-classes/typing_namedtuple/coordinates.py: -------------------------------------------------------------------------------- 1 | """ 2 | ``Coordinate``: a simple ``NamedTuple`` subclass with a custom ``__str__``:: 3 | 4 | >>> moscow = Coordinate(55.756, 37.617) 5 | >>> print(moscow) 6 | 55.8°N, 37.6°E 7 | 8 | """ 9 | 10 | # tag::COORDINATE[] 11 | from typing import NamedTuple 12 | 13 | class Coordinate(NamedTuple): 14 | lat: float 15 | lon: float 16 | 17 | def __str__(self): 18 | ns = 'N' if self.lat >= 0 else 'S' 19 | we = 'E' if self.lon >= 0 else 'W' 20 | return f'{abs(self.lat):.1f}°{ns}, {abs(self.lon):.1f}°{we}' 21 | # end::COORDINATE[] 22 | -------------------------------------------------------------------------------- /05-data-classes/typing_namedtuple/coordinates2.py: -------------------------------------------------------------------------------- 1 | """ 2 | ``Coordinate``: a simple ``NamedTuple`` subclass 3 | 4 | This version has a field with a default value:: 5 | 6 | >>> moscow = Coordinate(55.756, 37.617) 7 | >>> moscow 8 | Coordinate(lat=55.756, lon=37.617, reference='WGS84') 9 | 10 | """ 11 | 12 | # tag::COORDINATE[] 13 | from typing import NamedTuple 14 | 15 | class Coordinate(NamedTuple): 16 | lat: float # <1> 17 | lon: float 18 | reference: str = 'WGS84' # <2> 19 | # end::COORDINATE[] 20 | -------------------------------------------------------------------------------- /05-data-classes/typing_namedtuple/nocheck_demo.py: -------------------------------------------------------------------------------- 1 | import typing 2 | 3 | class Coordinate(typing.NamedTuple): 4 | lat: float 5 | lon: float 6 | 7 | trash = Coordinate('Ni!', None) # <1> 8 | print(trash) 9 | -------------------------------------------------------------------------------- /06-obj-ref/README.rst: -------------------------------------------------------------------------------- 1 | Sample code for Chapter 6 - "Object references, mutability and recycling" 2 | 3 | From the book "Fluent Python" by Luciano Ramalho (O'Reilly, 2015) 4 | http://shop.oreilly.com/product/0636920032519.do 5 | -------------------------------------------------------------------------------- /06-obj-ref/bus.py: -------------------------------------------------------------------------------- 1 | """ 2 | >>> import copy 3 | >>> bus1 = Bus(['Alice', 'Bill', 'Claire', 'David']) 4 | >>> bus2 = copy.copy(bus1) 5 | >>> bus3 = copy.deepcopy(bus1) 6 | >>> bus1.drop('Bill') 7 | >>> bus2.passengers 8 | ['Alice', 'Claire', 'David'] 9 | >>> bus3.passengers 10 | ['Alice', 'Bill', 'Claire', 'David'] 11 | 12 | """ 13 | 14 | # tag::BUS_CLASS[] 15 | class Bus: 16 | 17 | def __init__(self, passengers=None): 18 | if passengers is None: 19 | self.passengers = [] 20 | else: 21 | self.passengers = list(passengers) 22 | 23 | def pick(self, name): 24 | self.passengers.append(name) 25 | 26 | def drop(self, name): 27 | self.passengers.remove(name) 28 | # end::BUS_CLASS[] 29 | -------------------------------------------------------------------------------- /06-obj-ref/cheese.py: -------------------------------------------------------------------------------- 1 | """ 2 | >>> import weakref 3 | >>> stock = weakref.WeakValueDictionary() 4 | >>> catalog = [Cheese('Red Leicester'), Cheese('Tilsit'), 5 | ... Cheese('Brie'), Cheese('Parmesan')] 6 | ... 7 | >>> for cheese in catalog: 8 | ... stock[cheese.kind] = cheese 9 | ... 10 | >>> sorted(stock.keys()) 11 | ['Brie', 'Parmesan', 'Red Leicester', 'Tilsit'] 12 | >>> del catalog 13 | >>> sorted(stock.keys()) 14 | ['Parmesan'] 15 | >>> del cheese 16 | >>> sorted(stock.keys()) 17 | [] 18 | """ 19 | 20 | # tag::CHEESE_CLASS[] 21 | class Cheese: 22 | 23 | def __init__(self, kind): 24 | self.kind = kind 25 | 26 | def __repr__(self): 27 | return f'Cheese({self.kind!r})' 28 | # end::CHEESE_CLASS[] 29 | -------------------------------------------------------------------------------- /06-obj-ref/haunted_bus.py: -------------------------------------------------------------------------------- 1 | """ 2 | >>> bus1 = HauntedBus(['Alice', 'Bill']) 3 | >>> bus1.passengers 4 | ['Alice', 'Bill'] 5 | >>> bus1.pick('Charlie') 6 | >>> bus1.drop('Alice') 7 | >>> bus1.passengers 8 | ['Bill', 'Charlie'] 9 | >>> bus2 = HauntedBus() 10 | >>> bus2.pick('Carrie') 11 | >>> bus2.passengers 12 | ['Carrie'] 13 | >>> bus3 = HauntedBus() 14 | >>> bus3.passengers 15 | ['Carrie'] 16 | >>> bus3.pick('Dave') 17 | >>> bus2.passengers 18 | ['Carrie', 'Dave'] 19 | >>> bus2.passengers is bus3.passengers 20 | True 21 | >>> bus1.passengers 22 | ['Bill', 'Charlie'] 23 | 24 | 25 | >>> dir(HauntedBus.__init__) # doctest: +ELLIPSIS 26 | ['__annotations__', '__call__', ..., '__defaults__', ...] 27 | >>> HauntedBus.__init__.__defaults__ 28 | (['Carrie', 'Dave'],) 29 | >>> HauntedBus.__init__.__defaults__[0] is bus2.passengers 30 | True 31 | 32 | """ 33 | 34 | # tag::HAUNTED_BUS_CLASS[] 35 | class HauntedBus: 36 | """A bus model haunted by ghost passengers""" 37 | 38 | def __init__(self, passengers=[]): # <1> 39 | self.passengers = passengers # <2> 40 | 41 | def pick(self, name): 42 | self.passengers.append(name) # <3> 43 | 44 | def drop(self, name): 45 | self.passengers.remove(name) 46 | # end::HAUNTED_BUS_CLASS[] 47 | 48 | -------------------------------------------------------------------------------- /06-obj-ref/twilight_bus.py: -------------------------------------------------------------------------------- 1 | """ 2 | >>> basketball_team = ['Sue', 'Tina', 'Maya', 'Diana', 'Pat'] 3 | >>> bus = TwilightBus(basketball_team) 4 | >>> bus.drop('Tina') 5 | >>> bus.drop('Pat') 6 | >>> basketball_team 7 | ['Sue', 'Maya', 'Diana'] 8 | """ 9 | 10 | # tag::TWILIGHT_BUS_CLASS[] 11 | class TwilightBus: 12 | """A bus model that makes passengers vanish""" 13 | 14 | def __init__(self, passengers=None): 15 | if passengers is None: 16 | self.passengers = [] # <1> 17 | else: 18 | self.passengers = passengers #<2> 19 | 20 | def pick(self, name): 21 | self.passengers.append(name) 22 | 23 | def drop(self, name): 24 | self.passengers.remove(name) # <3> 25 | # end::TWILIGHT_BUS_CLASS[] 26 | 27 | -------------------------------------------------------------------------------- /07-1class-func/README.rst: -------------------------------------------------------------------------------- 1 | Sample code for Chapter 7 - "First-class functions" 2 | 3 | From the book "Fluent Python, Second Edition" by Luciano Ramalho (O'Reilly, 2020) 4 | http://shop.oreilly.com/product/0636920273196.do -------------------------------------------------------------------------------- /07-1class-func/bingocall.py: -------------------------------------------------------------------------------- 1 | """ 2 | # tag::BINGO_DEMO[] 3 | 4 | >>> bingo = BingoCage(range(3)) 5 | >>> bingo.pick() 6 | 1 7 | >>> bingo() 8 | 0 9 | >>> callable(bingo) 10 | True 11 | 12 | # end::BINGO_DEMO[] 13 | 14 | """ 15 | 16 | # tag::BINGO[] 17 | 18 | import random 19 | 20 | class BingoCage: 21 | 22 | def __init__(self, items): 23 | self._items = list(items) # <1> 24 | random.shuffle(self._items) # <2> 25 | 26 | def pick(self): # <3> 27 | try: 28 | return self._items.pop() 29 | except IndexError: 30 | raise LookupError('pick from empty BingoCage') # <4> 31 | 32 | def __call__(self): # <5> 33 | return self.pick() 34 | 35 | # end::BINGO[] 36 | -------------------------------------------------------------------------------- /07-1class-func/tagger.py: -------------------------------------------------------------------------------- 1 | """ 2 | # tag::TAG_DEMO[] 3 | >>> tag('br') # <1> 4 | '
' 5 | >>> tag('p', 'hello') # <2> 6 | '

hello

' 7 | >>> print(tag('p', 'hello', 'world')) 8 |

hello

9 |

world

10 | >>> tag('p', 'hello', id=33) # <3> 11 | '

hello

' 12 | >>> print(tag('p', 'hello', 'world', class_='sidebar')) # <4> 13 | 14 | 15 | >>> tag(content='testing', name="img") # <5> 16 | '' 17 | >>> my_tag = {'name': 'img', 'title': 'Sunset Boulevard', 18 | ... 'src': 'sunset.jpg', 'class': 'framed'} 19 | >>> tag(**my_tag) # <6> 20 | '' 21 | 22 | # end::TAG_DEMO[] 23 | """ 24 | 25 | 26 | # tag::TAG_FUNC[] 27 | def tag(name, *content, class_=None, **attrs): 28 | """Generate one or more HTML tags""" 29 | if class_ is not None: 30 | attrs['class'] = class_ 31 | attr_pairs = (f' {attr}="{value}"' for attr, value 32 | in sorted(attrs.items())) 33 | attr_str = ''.join(attr_pairs) 34 | if content: 35 | elements = (f'<{name}{attr_str}>{c}' 36 | for c in content) 37 | return '\n'.join(elements) 38 | else: 39 | return f'<{name}{attr_str} />' 40 | # end::TAG_FUNC[] 41 | -------------------------------------------------------------------------------- /08-def-type-hints/README.asciidoc: -------------------------------------------------------------------------------- 1 | == Type Hints in Function Definitions 2 | -------------------------------------------------------------------------------- /08-def-type-hints/arg_lab.py: -------------------------------------------------------------------------------- 1 | import typing 2 | from typing import Optional 3 | 4 | 5 | def f(a: str, *b: int, **c: float) -> None: 6 | if typing.TYPE_CHECKING: 7 | # reveal_type(b) 8 | reveal_type(c) 9 | print(a, b, c) 10 | 11 | 12 | def g(__a: int) -> None: 13 | print(__a) 14 | 15 | 16 | def h(a: int, /) -> None: 17 | print(a) 18 | 19 | 20 | def tag( 21 | name: str, 22 | /, 23 | *content: str, 24 | class_: Optional[str] = None, 25 | foo: Optional[str] = None, 26 | **attrs: str, 27 | ) -> str: 28 | return repr((name, content, class_, attrs)) 29 | 30 | 31 | f(a='1') 32 | f('1', 2, 3, x=4, y=5) 33 | g(__a=1) 34 | # h(a=1) 35 | print(tag('li', 'first', 'second', id='#123')) 36 | print(tag('li', 'first', 'second', class_='menu', id='#123')) 37 | -------------------------------------------------------------------------------- /08-def-type-hints/birds/birds.py: -------------------------------------------------------------------------------- 1 | class Bird: 2 | pass 3 | 4 | class Duck(Bird): # <1> 5 | def quack(self): 6 | print('Quack!') 7 | 8 | def alert(birdie): # <2> 9 | birdie.quack() 10 | 11 | def alert_duck(birdie: Duck) -> None: # <3> 12 | birdie.quack() 13 | 14 | def alert_bird(birdie: Bird) -> None: # <4> 15 | birdie.quack() 16 | -------------------------------------------------------------------------------- /08-def-type-hints/birds/daffy.py: -------------------------------------------------------------------------------- 1 | from birds import * 2 | 3 | daffy = Duck() 4 | alert(daffy) # <1> 5 | alert_duck(daffy) # <2> 6 | alert_bird(daffy) # <3> 7 | -------------------------------------------------------------------------------- /08-def-type-hints/birds/protocol/lake.py: -------------------------------------------------------------------------------- 1 | from typing import Protocol # <1> 2 | 3 | class GooseLike(Protocol): 4 | def honk(self, times: int) -> None: ... # <2> 5 | def swim(self) -> None: ... 6 | 7 | 8 | def alert(waterfowl: GooseLike) -> None: # <3> 9 | waterfowl.honk(2) 10 | -------------------------------------------------------------------------------- /08-def-type-hints/birds/protocol/parrot.py: -------------------------------------------------------------------------------- 1 | from lake import alert 2 | 3 | class Parrot: 4 | def honk(self, times: int) -> None: # <1> 5 | print('Honk! ' * times * 2) 6 | 7 | 8 | ze_carioca = Parrot() 9 | 10 | alert(ze_carioca) # <2> 11 | -------------------------------------------------------------------------------- /08-def-type-hints/birds/protocol/swan.py: -------------------------------------------------------------------------------- 1 | from lake import alert # <1> 2 | 3 | class Swan: # <2> 4 | def honk(self, repetitions: int) -> None: # <3> 5 | print('Honk! ' * repetitions) 6 | 7 | def swim(self) -> None: # <4> 8 | pass 9 | 10 | 11 | bella = Swan() 12 | 13 | alert(bella) # <5> 14 | -------------------------------------------------------------------------------- /08-def-type-hints/birds/woody.py: -------------------------------------------------------------------------------- 1 | from birds import * 2 | 3 | woody = Bird() 4 | alert(woody) 5 | alert_duck(woody) 6 | alert_bird(woody) 7 | -------------------------------------------------------------------------------- /08-def-type-hints/bus.py: -------------------------------------------------------------------------------- 1 | """ 2 | >>> import copy 3 | >>> bus1 = Bus(['Alice', 'Bill', 'Claire', 'David']) 4 | >>> bus2 = copy.copy(bus1) 5 | >>> bus3 = copy.deepcopy(bus1) 6 | >>> bus1.drop('Bill') 7 | >>> bus2.passengers 8 | ['Alice', 'Claire', 'David'] 9 | >>> bus3.passengers 10 | ['Alice', 'Bill', 'Claire', 'David'] 11 | 12 | """ 13 | 14 | # tag::BUS_CLASS[] 15 | class Bus: 16 | 17 | def __init__(self, passengers=None): 18 | if passengers is None: 19 | self.passengers = [] 20 | else: 21 | self.passengers = list(passengers) 22 | 23 | def pick(self, name): 24 | self.passengers.append(name) 25 | 26 | def drop(self, name): 27 | self.passengers.remove(name) 28 | # end::BUS_CLASS[] 29 | -------------------------------------------------------------------------------- /08-def-type-hints/callable/variance.py: -------------------------------------------------------------------------------- 1 | from collections.abc import Callable 2 | 3 | def update( # <1> 4 | probe: Callable[[], float], # <2> 5 | display: Callable[[float], None] # <3> 6 | ) -> None: 7 | temperature = probe() 8 | # imagine lots of control code here 9 | display(temperature) 10 | 11 | def probe_ok() -> int: # <4> 12 | return 42 13 | 14 | def display_wrong(temperature: int) -> None: # <5> 15 | print(hex(temperature)) 16 | 17 | update(probe_ok, display_wrong) # type error # <6> 18 | 19 | def display_ok(temperature: complex) -> None: # <7> 20 | print(temperature) 21 | 22 | update(probe_ok, display_ok) # OK # <8> 23 | -------------------------------------------------------------------------------- /08-def-type-hints/charindex.py: -------------------------------------------------------------------------------- 1 | """ 2 | ``name_index`` builds an inverted index mapping words to sets of Unicode 3 | characters which contain that word in their names. For example:: 4 | 5 | >>> index = name_index(32, 65) 6 | >>> sorted(index['SIGN']) 7 | ['#', '$', '%', '+', '<', '=', '>'] 8 | >>> sorted(index['DIGIT']) 9 | ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'] 10 | >>> index['DIGIT'] & index['EIGHT'] 11 | {'8'} 12 | """ 13 | 14 | # tag::CHARINDEX[] 15 | import sys 16 | import re 17 | import unicodedata 18 | from collections.abc import Iterator 19 | 20 | RE_WORD = re.compile(r'\w+') 21 | STOP_CODE = sys.maxunicode + 1 22 | 23 | def tokenize(text: str) -> Iterator[str]: # <1> 24 | """return iterable of uppercased words""" 25 | for match in RE_WORD.finditer(text): 26 | yield match.group().upper() 27 | 28 | def name_index(start: int = 32, end: int = STOP_CODE) -> dict[str, set[str]]: 29 | index: dict[str, set[str]] = {} # <2> 30 | for char in (chr(i) for i in range(start, end)): 31 | if name := unicodedata.name(char, ''): # <3> 32 | for word in tokenize(name): 33 | index.setdefault(word, set()).add(char) 34 | return index 35 | # end::CHARINDEX[] 36 | -------------------------------------------------------------------------------- /08-def-type-hints/columnize.py: -------------------------------------------------------------------------------- 1 | # tag::COLUMNIZE[] 2 | from collections.abc import Sequence 3 | 4 | def columnize( 5 | sequence: Sequence[str], num_columns: int = 0 6 | ) -> list[tuple[str, ...]]: 7 | if num_columns == 0: 8 | num_columns = round(len(sequence) ** 0.5) 9 | num_rows, reminder = divmod(len(sequence), num_columns) 10 | num_rows += bool(reminder) 11 | return [tuple(sequence[i::num_rows]) for i in range(num_rows)] 12 | # end::COLUMNIZE[] 13 | 14 | 15 | def demo() -> None: 16 | nato = ( 17 | 'Alfa Bravo Charlie Delta Echo Foxtrot Golf Hotel India' 18 | ' Juliett Kilo Lima Mike November Oscar Papa Quebec Romeo' 19 | ' Sierra Tango Uniform Victor Whiskey X-ray Yankee Zulu' 20 | ).split() 21 | 22 | for row in columnize(nato, 4): 23 | for word in row: 24 | print(f'{word:15}', end='') 25 | print() 26 | 27 | 28 | if __name__ == '__main__': 29 | demo() 30 | -------------------------------------------------------------------------------- /08-def-type-hints/comparable/comparable.py: -------------------------------------------------------------------------------- 1 | from typing import Protocol, Any 2 | 3 | class SupportsLessThan(Protocol): # <1> 4 | def __lt__(self, other: Any) -> bool: ... # <2> 5 | -------------------------------------------------------------------------------- /08-def-type-hints/comparable/top.py: -------------------------------------------------------------------------------- 1 | """ 2 | ``top(it, n)`` returns the "greatest" ``n`` elements of the iterable ``t``. 3 | Example: 4 | 5 | # tag::TOP_DOCTEST[] 6 | >>> top([4, 1, 5, 2, 6, 7, 3], 3) 7 | [7, 6, 5] 8 | >>> l = 'mango pear apple kiwi banana'.split() 9 | >>> top(l, 3) 10 | ['pear', 'mango', 'kiwi'] 11 | >>> 12 | >>> l2 = [(len(s), s) for s in l] 13 | >>> l2 14 | [(5, 'mango'), (4, 'pear'), (5, 'apple'), (4, 'kiwi'), (6, 'banana')] 15 | >>> top(l2, 3) 16 | [(6, 'banana'), (5, 'mango'), (5, 'apple')] 17 | 18 | # end::TOP_DOCTEST[] 19 | 20 | """ 21 | 22 | # tag::TOP[] 23 | from collections.abc import Iterable 24 | from typing import TypeVar 25 | 26 | from comparable import SupportsLessThan 27 | 28 | LT = TypeVar('LT', bound=SupportsLessThan) 29 | 30 | def top(series: Iterable[LT], length: int) -> list[LT]: 31 | ordered = sorted(series, reverse=True) 32 | return ordered[:length] 33 | # end::TOP[] 34 | -------------------------------------------------------------------------------- /08-def-type-hints/coordinates/coordinates.py: -------------------------------------------------------------------------------- 1 | # This example uses the geolib library: 2 | # https://pypi.org/project/geolib/ 3 | 4 | """ 5 | >>> shanghai = 31.2304, 121.4737 6 | >>> geohash(shanghai) 7 | 'wtw3sjq6q' 8 | """ 9 | 10 | # tag::GEOHASH[] 11 | from geolib import geohash as gh # type: ignore # <1> 12 | 13 | PRECISION = 9 14 | 15 | def geohash(lat_lon: tuple[float, float]) -> str: # <2> 16 | return gh.encode(*lat_lon, PRECISION) 17 | # end::GEOHASH[] 18 | -------------------------------------------------------------------------------- /08-def-type-hints/coordinates/coordinates_named.py: -------------------------------------------------------------------------------- 1 | # This example requires the geolib library: 2 | # https://pypi.org/project/geolib/ 3 | 4 | 5 | """ 6 | >>> shanghai = 31.2304, 121.4737 7 | >>> geohash(shanghai) 8 | 'wtw3sjq6q' 9 | """ 10 | 11 | # tag::GEOHASH[] 12 | from typing import NamedTuple 13 | 14 | from geolib import geohash as gh # type: ignore 15 | 16 | PRECISION = 9 17 | 18 | class Coordinate(NamedTuple): 19 | lat: float 20 | lon: float 21 | 22 | def geohash(lat_lon: Coordinate) -> str: 23 | return gh.encode(*lat_lon, PRECISION) 24 | # end::GEOHASH[] 25 | 26 | # tag::DISPLAY[] 27 | def display(lat_lon: tuple[float, float]) -> str: 28 | lat, lon = lat_lon 29 | ns = 'N' if lat >= 0 else 'S' 30 | ew = 'E' if lon >= 0 else 'W' 31 | return f'{abs(lat):0.1f}°{ns}, {abs(lon):0.1f}°{ew}' 32 | # end::DISPLAY[] 33 | 34 | def demo(): 35 | shanghai = 31.2304, 121.4737 36 | print(display(shanghai)) 37 | s = geohash(shanghai) 38 | print(s) 39 | 40 | if __name__ == '__main__': 41 | demo() 42 | -------------------------------------------------------------------------------- /08-def-type-hints/coordinates/coordinates_named_test.py: -------------------------------------------------------------------------------- 1 | from coordinates_named import geohash, Coordinate, display 2 | 3 | def test_geohash_max_precision() -> None: 4 | sao_paulo = -23.5505, -46.6339 5 | result = geohash(Coordinate(*sao_paulo)) 6 | assert '6gyf4bf0r' == result 7 | 8 | def test_display() -> None: 9 | sao_paulo = -23.5505, -46.6339 10 | assert display(sao_paulo) == '23.6°S, 46.6°W' 11 | shanghai = 31.2304, 121.4737 12 | assert display(shanghai) == '31.2°N, 121.5°E' 13 | -------------------------------------------------------------------------------- /08-def-type-hints/coordinates/coordinates_test.py: -------------------------------------------------------------------------------- 1 | from coordinates import geohash 2 | 3 | def test_geohash_max_precision() -> None: 4 | sao_paulo = -23.5505, -46.6339 5 | result = geohash(sao_paulo) 6 | assert '6gyf4bf0r' == result 7 | -------------------------------------------------------------------------------- /08-def-type-hints/coordinates/requirements.txt: -------------------------------------------------------------------------------- 1 | geolib==1.0.7 2 | future==0.18.3 3 | -------------------------------------------------------------------------------- /08-def-type-hints/ctime.py: -------------------------------------------------------------------------------- 1 | import time 2 | from typing import Optional 3 | 4 | def ctime(secs: Optional[float] = None, /) -> str: 5 | return time.ctime(secs) 6 | -------------------------------------------------------------------------------- /08-def-type-hints/double/double_object.py: -------------------------------------------------------------------------------- 1 | def double(n: object) -> object: 2 | return n * 2 3 | -------------------------------------------------------------------------------- /08-def-type-hints/double/double_protocol.py: -------------------------------------------------------------------------------- 1 | from typing import TypeVar, Protocol 2 | 3 | T = TypeVar('T') # <1> 4 | 5 | class Repeatable(Protocol): 6 | def __mul__(self: T, other: int) -> T: ... # <2> 7 | 8 | RT = TypeVar('RT', bound=Repeatable) # <3> 9 | 10 | def double(n: RT) -> RT: # <4> 11 | return n * 2 12 | -------------------------------------------------------------------------------- /08-def-type-hints/double/double_sequence.py: -------------------------------------------------------------------------------- 1 | from collections import abc 2 | from typing import Any 3 | 4 | def double(n: abc.Sequence) -> Any: 5 | return n * 2 6 | 7 | -------------------------------------------------------------------------------- /08-def-type-hints/messages/hints_1/messages.py: -------------------------------------------------------------------------------- 1 | """ 2 | # tag::SHOW_COUNT_DOCTEST[] 3 | >>> show_count(99, 'bird') 4 | '99 birds' 5 | >>> show_count(1, 'bird') 6 | '1 bird' 7 | >>> show_count(0, 'bird') 8 | 'no birds' 9 | 10 | # end::SHOW_COUNT_DOCTEST[] 11 | """ 12 | 13 | # tag::SHOW_COUNT[] 14 | def show_count(count: int, word: str) -> str: 15 | if count == 1: 16 | return f'1 {word}' 17 | count_str = str(count) if count else 'no' 18 | return f'{count_str} {word}s' 19 | # end::SHOW_COUNT[] 20 | -------------------------------------------------------------------------------- /08-def-type-hints/messages/hints_1/messages_test.py: -------------------------------------------------------------------------------- 1 | from pytest import mark 2 | 3 | from messages import show_count 4 | 5 | 6 | @mark.parametrize('qty, expected', [ 7 | (1, '1 part'), 8 | (2, '2 parts'), 9 | ]) 10 | def test_show_count(qty: int, expected: str) -> None: 11 | got = show_count(qty, 'part') 12 | assert got == expected 13 | 14 | 15 | def test_show_count_zero(): 16 | got = show_count(0, 'part') 17 | assert got == 'no parts' 18 | -------------------------------------------------------------------------------- /08-def-type-hints/messages/hints_2/messages.py: -------------------------------------------------------------------------------- 1 | """ 2 | >>> show_count(99, 'bird') 3 | '99 birds' 4 | >>> show_count(1, 'bird') 5 | '1 bird' 6 | >>> show_count(0, 'bird') 7 | 'no birds' 8 | >>> show_count(3, 'virus', 'viruses') 9 | '3 viruses' 10 | >>> show_count(1, 'virus', 'viruses') 11 | '1 virus' 12 | >>> show_count(0, 'virus', 'viruses') 13 | 'no viruses' 14 | """ 15 | 16 | # tag::SHOW_COUNT[] 17 | def show_count(count: int, singular: str, plural: str = '') -> str: 18 | if count == 1: 19 | return f'1 {singular}' 20 | count_str = str(count) if count else 'no' 21 | if not plural: 22 | plural = singular + 's' 23 | return f'{count_str} {plural}' 24 | 25 | # end::SHOW_COUNT[] 26 | -------------------------------------------------------------------------------- /08-def-type-hints/messages/hints_2/messages_test.py: -------------------------------------------------------------------------------- 1 | from pytest import mark 2 | 3 | from messages import show_count 4 | 5 | 6 | @mark.parametrize('qty, expected', [ 7 | (1, '1 part'), 8 | (2, '2 parts'), 9 | (0, 'no parts'), 10 | ]) 11 | def test_show_count(qty: int, expected: str) -> None: 12 | got = show_count(qty, 'part') 13 | assert got == expected 14 | 15 | 16 | # tag::TEST_IRREGULAR[] 17 | @mark.parametrize('qty, expected', [ 18 | (1, '1 child'), 19 | (2, '2 children'), 20 | (0, 'no children'), 21 | ]) 22 | def test_irregular(qty: int, expected: str) -> None: 23 | got = show_count(qty, 'child', 'children') 24 | assert got == expected 25 | # end::TEST_IRREGULAR[] 26 | -------------------------------------------------------------------------------- /08-def-type-hints/messages/no_hints/messages.py: -------------------------------------------------------------------------------- 1 | """ 2 | # tag::SHOW_COUNT_DOCTEST[] 3 | >>> show_count(99, 'bird') 4 | '99 birds' 5 | >>> show_count(1, 'bird') 6 | '1 bird' 7 | >>> show_count(0, 'bird') 8 | 'no birds' 9 | 10 | # end::SHOW_COUNT_DOCTEST[] 11 | """ 12 | 13 | # tag::SHOW_COUNT[] 14 | def show_count(count, word): 15 | if count == 1: 16 | return f'1 {word}' 17 | count_str = str(count) if count else 'no' 18 | return f'{count_str} {word}s' 19 | # end::SHOW_COUNT[] 20 | -------------------------------------------------------------------------------- /08-def-type-hints/messages/no_hints/messages_test.py: -------------------------------------------------------------------------------- 1 | from pytest import mark 2 | 3 | from messages import show_count 4 | 5 | @mark.parametrize('qty, expected', [ 6 | (1, '1 part'), 7 | (2, '2 parts'), 8 | ]) 9 | def test_show_count(qty, expected): 10 | got = show_count(qty, 'part') 11 | assert got == expected 12 | 13 | def test_show_count_zero(): 14 | got = show_count(0, 'part') 15 | assert got == 'no parts' 16 | -------------------------------------------------------------------------------- /08-def-type-hints/mode/mode_float.py: -------------------------------------------------------------------------------- 1 | # tag::MODE_FLOAT[] 2 | from collections import Counter 3 | from collections.abc import Iterable 4 | 5 | def mode(data: Iterable[float]) -> float: 6 | pairs = Counter(data).most_common(1) 7 | if len(pairs) == 0: 8 | raise ValueError('no mode for empty data') 9 | return pairs[0][0] 10 | # end::MODE_FLOAT[] 11 | 12 | def demo() -> None: 13 | import typing 14 | pop = [1, 1, 2, 3, 3, 3, 3, 4] 15 | m = mode(pop) 16 | if typing.TYPE_CHECKING: 17 | reveal_type(pop) 18 | reveal_type(m) 19 | print(pop) 20 | print(repr(m), type(m)) 21 | 22 | if __name__ == '__main__': 23 | demo() 24 | -------------------------------------------------------------------------------- /08-def-type-hints/mode/mode_hashable.py: -------------------------------------------------------------------------------- 1 | # tag::MODE_HASHABLE_T[] 2 | from collections import Counter 3 | from collections.abc import Iterable, Hashable 4 | from typing import TypeVar 5 | 6 | HashableT = TypeVar('HashableT', bound=Hashable) 7 | 8 | def mode(data: Iterable[HashableT]) -> HashableT: 9 | pairs = Counter(data).most_common(1) 10 | if len(pairs) == 0: 11 | raise ValueError('no mode for empty data') 12 | return pairs[0][0] 13 | # end::MODE_HASHABLE_T[] 14 | 15 | 16 | def demo() -> None: 17 | import typing 18 | 19 | pop = 'abracadabra' 20 | m = mode(pop) 21 | if typing.TYPE_CHECKING: 22 | reveal_type(pop) 23 | reveal_type(m) 24 | print(pop) 25 | print(m.upper(), type(m)) 26 | 27 | 28 | if __name__ == '__main__': 29 | demo() 30 | -------------------------------------------------------------------------------- /08-def-type-hints/mypy.ini: -------------------------------------------------------------------------------- 1 | [mypy] 2 | python_version = 3.9 3 | warn_unused_configs = True 4 | disallow_incomplete_defs = True 5 | -------------------------------------------------------------------------------- /08-def-type-hints/replacer.py: -------------------------------------------------------------------------------- 1 | """ 2 | ``zip_replace`` replaces multiple calls to ``str.replace``:: 3 | 4 | >>> changes = [ 5 | ... ('(', ' ( '), 6 | ... (')', ' ) '), 7 | ... (' ', ' '), 8 | ... ] 9 | >>> expr = '(+ 2 (* 3 7))' 10 | >>> zip_replace(expr, changes) 11 | ' ( + 2 ( * 3 7 ) ) ' 12 | 13 | """ 14 | 15 | # tag::ZIP_REPLACE[] 16 | from collections.abc import Iterable 17 | 18 | FromTo = tuple[str, str] # <1> 19 | 20 | def zip_replace(text: str, changes: Iterable[FromTo]) -> str: # <2> 21 | for from_, to in changes: 22 | text = text.replace(from_, to) 23 | return text 24 | # end::ZIP_REPLACE[] 25 | 26 | def demo() -> None: 27 | import doctest 28 | failed, count = doctest.testmod() 29 | print(f'{count-failed} of {count} doctests OK') 30 | l33t = [(p[0], p[1]) for p in 'a4 e3 i1 o0'.split()] 31 | text = 'mad skilled noob powned leet' 32 | print(zip_replace(text, l33t)) 33 | 34 | 35 | if __name__ == '__main__': 36 | demo() 37 | -------------------------------------------------------------------------------- /08-def-type-hints/romans.py: -------------------------------------------------------------------------------- 1 | values_map = [ 2 | (1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1), 3 | ( 'M', 'CM', 'D', 'CD', 'C', 'XC', 'L', 'XL', 'X', 'IX', 'V', 'IV', 'I') 4 | ] 5 | 6 | def to_roman(arabic: int) -> str: 7 | """ Convert an integer to a Roman numeral. """ 8 | if not 0 < arabic < 4000: 9 | raise ValueError('Argument must be between 1 and 3999') 10 | 11 | result = [] 12 | for value, numeral in zip(*values_map): 13 | repeat = arabic // value 14 | result.append(numeral * repeat) 15 | arabic -= value * repeat 16 | return ''.join(result) 17 | -------------------------------------------------------------------------------- /08-def-type-hints/romans_test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from romans import to_roman 4 | 5 | 6 | def test_to_roman_1(): 7 | assert to_roman(1) == 'I' 8 | 9 | 10 | @pytest.mark.parametrize('arabic, roman', [ 11 | (3, 'III'), 12 | (4, 'IV'), 13 | (1009, 'MIX'), 14 | (1969, 'MCMLXIX'), 15 | (3999, 'MMMCMXCIX') 16 | ]) 17 | def test_to_roman(arabic, roman): 18 | assert to_roman(arabic) == roman 19 | -------------------------------------------------------------------------------- /08-def-type-hints/sample.py: -------------------------------------------------------------------------------- 1 | # tag::SAMPLE[] 2 | from collections.abc import Sequence 3 | from random import shuffle 4 | from typing import TypeVar 5 | 6 | T = TypeVar('T') 7 | 8 | def sample(population: Sequence[T], size: int) -> list[T]: 9 | if size < 1: 10 | raise ValueError('size must be >= 1') 11 | result = list(population) 12 | shuffle(result) 13 | return result[:size] 14 | # end::SAMPLE[] 15 | 16 | def demo() -> None: 17 | import typing 18 | p1 = tuple(range(10)) 19 | s1 = sample(p1, 3) 20 | if typing.TYPE_CHECKING: 21 | reveal_type(p1) 22 | reveal_type(s1) 23 | print(p1) 24 | print(s1) 25 | p2 = 'The quick brown fox jumps over the lazy dog' 26 | s2 = sample(p2, 10) 27 | if typing.TYPE_CHECKING: 28 | reveal_type(p2) 29 | reveal_type(s2) 30 | print(p2) 31 | print(s2) 32 | 33 | 34 | if __name__ == '__main__': 35 | demo() 36 | -------------------------------------------------------------------------------- /08-def-type-hints/typevar_bounded.py: -------------------------------------------------------------------------------- 1 | from typing import TypeVar, TYPE_CHECKING 2 | 3 | BT = TypeVar('BT', bound=float) 4 | 5 | def triple2(a: BT) -> BT: 6 | return a * 3 7 | 8 | res2 = triple2(2) 9 | 10 | if TYPE_CHECKING: 11 | reveal_type(res2) 12 | -------------------------------------------------------------------------------- /08-def-type-hints/typevars_constrained.py: -------------------------------------------------------------------------------- 1 | from typing import TypeVar, TYPE_CHECKING 2 | from decimal import Decimal 3 | 4 | # tag::TYPEVAR_RESTRICTED[] 5 | RT = TypeVar('RT', float, Decimal) 6 | 7 | def triple1(a: RT) -> RT: 8 | return a * 3 9 | 10 | res1 = triple1(2) 11 | 12 | if TYPE_CHECKING: 13 | reveal_type(res1) 14 | # end::TYPEVAR_RESTRICTED[] 15 | 16 | # tag::TYPEVAR_BOUNDED[] 17 | BT = TypeVar('BT', bound=float) 18 | 19 | def triple2(a: BT) -> BT: 20 | return a * 3 21 | 22 | res2 = triple2(2) 23 | 24 | if TYPE_CHECKING: 25 | reveal_type(res2) 26 | # tag::TYPEVAR_BOUNDED[] 27 | -------------------------------------------------------------------------------- /09-closure-deco/README.rst: -------------------------------------------------------------------------------- 1 | Sample code for Chapter 8 - "Closures and decorators" 2 | 3 | From the book "Fluent Python, Second Edition" by Luciano Ramalho (O'Reilly, 2020) 4 | http://shop.oreilly.com/product/0636920273196.do -------------------------------------------------------------------------------- /09-closure-deco/average.py: -------------------------------------------------------------------------------- 1 | """ 2 | >>> avg = make_averager() 3 | >>> avg(10) 4 | 10.0 5 | >>> avg(11) 6 | 10.5 7 | >>> avg(12) 8 | 11.0 9 | >>> avg.__code__.co_varnames 10 | ('new_value', 'total') 11 | >>> avg.__code__.co_freevars 12 | ('series',) 13 | >>> avg.__closure__ # doctest: +ELLIPSIS 14 | (,) 15 | >>> avg.__closure__[0].cell_contents 16 | [10, 11, 12] 17 | """ 18 | 19 | DEMO = """ 20 | >>> avg.__closure__ 21 | (,) 22 | """ 23 | 24 | 25 | def make_averager(): 26 | series = [] 27 | 28 | def averager(new_value): 29 | series.append(new_value) 30 | total = sum(series) 31 | return total / len(series) 32 | 33 | return averager 34 | -------------------------------------------------------------------------------- /09-closure-deco/average_oo.py: -------------------------------------------------------------------------------- 1 | """ 2 | >>> avg = Averager() 3 | >>> avg(10) 4 | 10.0 5 | >>> avg(11) 6 | 10.5 7 | >>> avg(12) 8 | 11.0 9 | 10 | """ 11 | 12 | 13 | class Averager: 14 | 15 | def __init__(self): 16 | self.series = [] 17 | 18 | def __call__(self, new_value): 19 | self.series.append(new_value) 20 | total = sum(self.series) 21 | return total/len(self.series) 22 | -------------------------------------------------------------------------------- /09-closure-deco/clock/clockdeco.py: -------------------------------------------------------------------------------- 1 | import time 2 | import functools 3 | 4 | 5 | def clock(func): 6 | @functools.wraps(func) 7 | def clocked(*args, **kwargs): 8 | t0 = time.perf_counter() 9 | result = func(*args, **kwargs) 10 | elapsed = time.perf_counter() - t0 11 | name = func.__name__ 12 | arg_lst = [repr(arg) for arg in args] 13 | arg_lst.extend(f'{k}={v!r}' for k, v in kwargs.items()) 14 | arg_str = ', '.join(arg_lst) 15 | print(f'[{elapsed:0.8f}s] {name}({arg_str}) -> {result!r}') 16 | return result 17 | return clocked 18 | -------------------------------------------------------------------------------- /09-closure-deco/clock/clockdeco0.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | 4 | def clock(func): 5 | def clocked(*args): # <1> 6 | t0 = time.perf_counter() 7 | result = func(*args) # <2> 8 | elapsed = time.perf_counter() - t0 9 | name = func.__name__ 10 | arg_str = ', '.join(repr(arg) for arg in args) 11 | print(f'[{elapsed:0.8f}s] {name}({arg_str}) -> {result!r}') 12 | return result 13 | return clocked # <3> 14 | -------------------------------------------------------------------------------- /09-closure-deco/clock/clockdeco_cls.py: -------------------------------------------------------------------------------- 1 | # clockdeco_class.py 2 | 3 | """ 4 | >>> snooze(.1) # doctest: +ELLIPSIS 5 | [0.101...s] snooze(0.1) -> None 6 | >>> clock('{name}: {elapsed}')(time.sleep)(.2) # doctest: +ELLIPSIS 7 | sleep: 0.20... 8 | >>> clock('{name}({args}) dt={elapsed:0.3f}s')(time.sleep)(.2) 9 | sleep(0.2) dt=0.201s 10 | """ 11 | 12 | # tag::CLOCKDECO_CLS[] 13 | import time 14 | 15 | DEFAULT_FMT = '[{elapsed:0.8f}s] {name}({args}) -> {result}' 16 | 17 | class clock: # <1> 18 | 19 | def __init__(self, fmt=DEFAULT_FMT): # <2> 20 | self.fmt = fmt 21 | 22 | def __call__(self, func): # <3> 23 | def clocked(*_args): 24 | t0 = time.perf_counter() 25 | _result = func(*_args) # <4> 26 | elapsed = time.perf_counter() - t0 27 | name = func.__name__ 28 | args = ', '.join(repr(arg) for arg in _args) 29 | result = repr(_result) 30 | print(self.fmt.format(**locals())) 31 | return _result 32 | return clocked 33 | # end::CLOCKDECO_CLS[] 34 | 35 | if __name__ == '__main__': 36 | 37 | @clock() 38 | def snooze(seconds): 39 | time.sleep(seconds) 40 | 41 | for i in range(3): 42 | snooze(.123) 43 | 44 | -------------------------------------------------------------------------------- /09-closure-deco/clock/clockdeco_demo.py: -------------------------------------------------------------------------------- 1 | import time 2 | from clockdeco0 import clock 3 | 4 | @clock 5 | def snooze(seconds): 6 | time.sleep(seconds) 7 | 8 | @clock 9 | def factorial(n): 10 | return 1 if n < 2 else n*factorial(n-1) 11 | 12 | if __name__ == '__main__': 13 | print('*' * 40, 'Calling snooze(.123)') 14 | snooze(.123) 15 | print('*' * 40, 'Calling factorial(6)') 16 | print('6! =', factorial(6)) 17 | -------------------------------------------------------------------------------- /09-closure-deco/clock/clockdeco_param.py: -------------------------------------------------------------------------------- 1 | # clockdeco_param.py 2 | 3 | """ 4 | >>> snooze(.1) # doctest: +ELLIPSIS 5 | [0.101...s] snooze(0.1) -> None 6 | >>> clock('{name}: {elapsed}')(time.sleep)(.2) # doctest: +ELLIPSIS 7 | sleep: 0.20... 8 | >>> clock('{name}({args}) dt={elapsed:0.3f}s')(time.sleep)(.2) 9 | sleep(0.2) dt=0.201s 10 | """ 11 | 12 | # tag::CLOCKDECO_PARAM[] 13 | import time 14 | 15 | DEFAULT_FMT = '[{elapsed:0.8f}s] {name}({args}) -> {result}' 16 | 17 | def clock(fmt=DEFAULT_FMT): # <1> 18 | def decorate(func): # <2> 19 | def clocked(*_args): # <3> 20 | t0 = time.perf_counter() 21 | _result = func(*_args) # <4> 22 | elapsed = time.perf_counter() - t0 23 | name = func.__name__ 24 | args = ', '.join(repr(arg) for arg in _args) # <5> 25 | result = repr(_result) # <6> 26 | print(fmt.format(**locals())) # <7> 27 | return _result # <8> 28 | return clocked # <9> 29 | return decorate # <10> 30 | 31 | if __name__ == '__main__': 32 | 33 | @clock() # <11> 34 | def snooze(seconds): 35 | time.sleep(seconds) 36 | 37 | for i in range(3): 38 | snooze(.123) 39 | 40 | # end::CLOCKDECO_PARAM[] 41 | -------------------------------------------------------------------------------- /09-closure-deco/clock/clockdeco_param_demo1.py: -------------------------------------------------------------------------------- 1 | import time 2 | from clockdeco_param import clock 3 | 4 | @clock('{name}: {elapsed}s') 5 | def snooze(seconds): 6 | time.sleep(seconds) 7 | 8 | for i in range(3): 9 | snooze(.123) 10 | -------------------------------------------------------------------------------- /09-closure-deco/clock/clockdeco_param_demo2.py: -------------------------------------------------------------------------------- 1 | import time 2 | from clockdeco_param import clock 3 | 4 | @clock('{name}({args}) dt={elapsed:0.3f}s') 5 | def snooze(seconds): 6 | time.sleep(seconds) 7 | 8 | for i in range(3): 9 | snooze(.123) 10 | -------------------------------------------------------------------------------- /09-closure-deco/fibo_compare.py: -------------------------------------------------------------------------------- 1 | from clockdeco import clock 2 | import fibo_demo 3 | import fibo_demo_lru 4 | 5 | 6 | @clock 7 | def demo1(): 8 | fibo_demo.fibonacci(30) 9 | 10 | 11 | @clock 12 | def demo2(): 13 | fibo_demo_lru.fibonacci(30) 14 | 15 | 16 | demo1() 17 | demo2() 18 | -------------------------------------------------------------------------------- /09-closure-deco/fibo_demo.py: -------------------------------------------------------------------------------- 1 | from clockdeco import clock 2 | 3 | 4 | @clock 5 | def fibonacci(n): 6 | if n < 2: 7 | return n 8 | return fibonacci(n - 2) + fibonacci(n - 1) 9 | 10 | 11 | if __name__ == '__main__': 12 | print(fibonacci(6)) 13 | -------------------------------------------------------------------------------- /09-closure-deco/fibo_demo_cache.py: -------------------------------------------------------------------------------- 1 | import functools 2 | 3 | from clockdeco import clock 4 | 5 | 6 | @functools.cache # <1> 7 | @clock # <2> 8 | def fibonacci(n): 9 | if n < 2: 10 | return n 11 | return fibonacci(n - 2) + fibonacci(n - 1) 12 | 13 | 14 | if __name__ == '__main__': 15 | print(fibonacci(6)) 16 | -------------------------------------------------------------------------------- /09-closure-deco/registration.py: -------------------------------------------------------------------------------- 1 | # tag::REGISTRATION[] 2 | 3 | registry = [] # <1> 4 | 5 | def register(func): # <2> 6 | print(f'running register({func})') # <3> 7 | registry.append(func) # <4> 8 | return func # <5> 9 | 10 | @register # <6> 11 | def f1(): 12 | print('running f1()') 13 | 14 | @register 15 | def f2(): 16 | print('running f2()') 17 | 18 | def f3(): # <7> 19 | print('running f3()') 20 | 21 | def main(): # <8> 22 | print('running main()') 23 | print('registry ->', registry) 24 | f1() 25 | f2() 26 | f3() 27 | 28 | if __name__ == '__main__': 29 | main() # <9> 30 | 31 | # end::REGISTRATION[] 32 | -------------------------------------------------------------------------------- /09-closure-deco/registration_abridged.py: -------------------------------------------------------------------------------- 1 | # tag::REGISTRATION_ABRIDGED[] 2 | registry = [] 3 | 4 | def register(func): 5 | print(f'running register({func})') 6 | registry.append(func) 7 | return func 8 | 9 | @register 10 | def f1(): 11 | print('running f1()') 12 | 13 | print('running main()') 14 | print('registry ->', registry) 15 | f1() 16 | # end::REGISTRATION_ABRIDGED[] 17 | -------------------------------------------------------------------------------- /09-closure-deco/registration_param.py: -------------------------------------------------------------------------------- 1 | # tag::REGISTRATION_PARAM[] 2 | 3 | registry = set() # <1> 4 | 5 | def register(active=True): # <2> 6 | def decorate(func): # <3> 7 | print('running register' 8 | f'(active={active})->decorate({func})') 9 | if active: # <4> 10 | registry.add(func) 11 | else: 12 | registry.discard(func) # <5> 13 | 14 | return func # <6> 15 | return decorate # <7> 16 | 17 | @register(active=False) # <8> 18 | def f1(): 19 | print('running f1()') 20 | 21 | @register() # <9> 22 | def f2(): 23 | print('running f2()') 24 | 25 | def f3(): 26 | print('running f3()') 27 | 28 | # end::REGISTRATION_PARAM[] 29 | -------------------------------------------------------------------------------- /09-closure-deco/stacked.py: -------------------------------------------------------------------------------- 1 | def first(f): 2 | print(f'apply first({f.__name__})') 3 | 4 | def inner1st(n): 5 | result = f(n) 6 | print(f'inner1({n}): called {f.__name__}({n}) -> {result}') 7 | return result 8 | return inner1st 9 | 10 | 11 | def second(f): 12 | print(f'apply second({f.__name__})') 13 | 14 | def inner2nd(n): 15 | result = f(n) 16 | print(f'inner2({n}): called {f.__name__}({n}) -> {result}') 17 | return result 18 | return inner2nd 19 | 20 | 21 | @first 22 | @second 23 | def double(n): 24 | return n * 2 25 | 26 | 27 | print(double(3)) 28 | 29 | 30 | def double_(n): 31 | return n * 2 32 | 33 | 34 | double_ = first(second(double_)) 35 | 36 | print(double_(3)) 37 | -------------------------------------------------------------------------------- /10-dp-1class-func/monkeytype/classic_strategy.pyi: -------------------------------------------------------------------------------- 1 | from typing import ( 2 | List, 3 | Optional, 4 | Union, 5 | ) 6 | 7 | 8 | class BulkItemPromo: 9 | def discount(self, order: Order) -> Union[float, int]: ... 10 | 11 | 12 | class FidelityPromo: 13 | def discount(self, order: Order) -> Union[float, int]: ... 14 | 15 | 16 | class LargeOrderPromo: 17 | def discount(self, order: Order) -> Union[float, int]: ... 18 | 19 | 20 | class LineItem: 21 | def __init__(self, product: str, quantity: int, price: float) -> None: ... 22 | def total(self) -> float: ... 23 | 24 | 25 | class Order: 26 | def __init__( 27 | self, 28 | customer: Customer, 29 | cart: List[LineItem], 30 | promotion: Optional[Union[BulkItemPromo, LargeOrderPromo, FidelityPromo]] = ... 31 | ) -> None: ... 32 | def due(self) -> float: ... 33 | def total(self) -> float: ... 34 | -------------------------------------------------------------------------------- /10-dp-1class-func/monkeytype/run.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | pytest.main(['.']) 3 | -------------------------------------------------------------------------------- /10-dp-1class-func/promotions.py: -------------------------------------------------------------------------------- 1 | from decimal import Decimal 2 | from strategy import Order 3 | 4 | def fidelity_promo(order: Order) -> Decimal: # <3> 5 | """5% discount for customers with 1000 or more fidelity points""" 6 | if order.customer.fidelity >= 1000: 7 | return order.total() * Decimal('0.05') 8 | return Decimal(0) 9 | 10 | 11 | def bulk_item_promo(order: Order) -> Decimal: 12 | """10% discount for each LineItem with 20 or more units""" 13 | discount = Decimal(0) 14 | for item in order.cart: 15 | if item.quantity >= 20: 16 | discount += item.total() * Decimal('0.1') 17 | return discount 18 | 19 | 20 | def large_order_promo(order: Order) -> Decimal: 21 | """7% discount for orders with 10 or more distinct items""" 22 | distinct_items = {item.product for item in order.cart} 23 | if len(distinct_items) >= 10: 24 | return order.total() * Decimal('0.07') 25 | return Decimal(0) 26 | -------------------------------------------------------------------------------- /10-dp-1class-func/requirements.txt: -------------------------------------------------------------------------------- 1 | mypy==0.910 2 | pytest==6.2.4 3 | -------------------------------------------------------------------------------- /10-dp-1class-func/untyped/promotions.py: -------------------------------------------------------------------------------- 1 | def fidelity_promo(order): 2 | """5% discount for customers with 1000 or more fidelity points""" 3 | return order.total() * .05 if order.customer.fidelity >= 1000 else 0 4 | 5 | 6 | def bulk_item_promo(order): 7 | """10% discount for each LineItem with 20 or more units""" 8 | discount = 0 9 | for item in order.cart: 10 | if item.quantity >= 20: 11 | discount += item.total() * .1 12 | return discount 13 | 14 | def large_order_promo(order): 15 | """7% discount for orders with 10 or more distinct items""" 16 | distinct_items = {item.product for item in order.cart} 17 | if len(distinct_items) >= 10: 18 | return order.total() * .07 19 | return 0 20 | -------------------------------------------------------------------------------- /11-pythonic-obj/README.md: -------------------------------------------------------------------------------- 1 | # A Pythonic Object 2 | 3 | Sample code for Chapter 11 of _Fluent Python 2e_ by Luciano Ramalho (O'Reilly, 2020) 4 | 5 | The _memtest.py_ script takes a module name in the command line and loads it. 6 | Assuming the module defines a class named `Vector`, _memtest.py_ creates a list with 10 million instances, reporting the memory usage before and after the list is created. 7 | -------------------------------------------------------------------------------- /11-pythonic-obj/mem_test.py: -------------------------------------------------------------------------------- 1 | import importlib 2 | import sys 3 | import resource 4 | 5 | NUM_VECTORS = 10**7 6 | 7 | module = None 8 | if len(sys.argv) == 2: 9 | module_name = sys.argv[1].replace('.py', '') 10 | module = importlib.import_module(module_name) 11 | else: 12 | print(f'Usage: {sys.argv[0]} ') 13 | 14 | if module is None: 15 | print('Running test with built-in `complex`') 16 | cls = complex 17 | else: 18 | fmt = 'Selected Vector2d type: {.__name__}.{.__name__}' 19 | print(fmt.format(module, module.Vector2d)) 20 | cls = module.Vector2d 21 | 22 | mem_init = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss 23 | print(f'Creating {NUM_VECTORS:,} {cls.__qualname__!r} instances') 24 | 25 | vectors = [cls(3.0, 4.0) for i in range(NUM_VECTORS)] 26 | 27 | mem_final = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss 28 | print(f'Initial RAM usage: {mem_init:14,}') 29 | print(f' Final RAM usage: {mem_final:14,}') 30 | -------------------------------------------------------------------------------- /11-pythonic-obj/private/.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | .jython_cache/ 3 | -------------------------------------------------------------------------------- /11-pythonic-obj/private/Confidential.java: -------------------------------------------------------------------------------- 1 | public class Confidential { 2 | 3 | private String secret = ""; 4 | 5 | public Confidential(String text) { 6 | this.secret = text.toUpperCase(); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /11-pythonic-obj/private/Expose.java: -------------------------------------------------------------------------------- 1 | import java.lang.reflect.Field; 2 | 3 | public class Expose { 4 | 5 | public static void main(String[] args) { 6 | Confidential message = new Confidential("top secret text"); 7 | Field secretField = null; 8 | try { 9 | secretField = Confidential.class.getDeclaredField("secret"); 10 | } 11 | catch (NoSuchFieldException e) { 12 | System.err.println(e); 13 | System.exit(1); 14 | } 15 | secretField.setAccessible(true); // break the lock! 16 | try { 17 | String wasHidden = (String) secretField.get(message); 18 | System.out.println("message.secret = " + wasHidden); 19 | } 20 | catch (IllegalAccessException e) { 21 | // this will not happen after setAccessible(true) 22 | System.err.println(e); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /11-pythonic-obj/private/expose.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env jython 2 | # NOTE: Jython is still Python 2.7 in late2020 3 | 4 | import Confidential 5 | 6 | message = Confidential('top secret text') 7 | secret_field = Confidential.getDeclaredField('secret') 8 | secret_field.setAccessible(True) # break the lock! 9 | print 'message.secret =', secret_field.get(message) 10 | -------------------------------------------------------------------------------- /11-pythonic-obj/private/leakprivate.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env jython 2 | # NOTE: Jython is still Python 2.7 in late2020 3 | 4 | from java.lang.reflect import Modifier 5 | import Confidential 6 | 7 | message = Confidential('top secret text') 8 | fields = Confidential.getDeclaredFields() 9 | for field in fields: 10 | # list private fields only 11 | if Modifier.isPrivate(field.getModifiers()): 12 | field.setAccessible(True) # break the lock 13 | print 'field:', field 14 | print '\t', field.getName(), '=', field.get(message) 15 | -------------------------------------------------------------------------------- /11-pythonic-obj/private/no_respect.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env jython 2 | # NOTE: Jython is still Python 2.7 in late2020 3 | 4 | """ 5 | In the Jython registry file there is this line: 6 | 7 | python.security.respectJavaAccessibility = true 8 | 9 | Set this to false and Jython provides access to non-public 10 | fields, methods, and constructors of Java objects. 11 | """ 12 | 13 | import Confidential 14 | 15 | message = Confidential('top secret text') 16 | for name in dir(message): 17 | attr = getattr(message, name) 18 | if not callable(attr): # non-methods only 19 | print name + '\t=', attr 20 | -------------------------------------------------------------------------------- /11-pythonic-obj/slots.rst: -------------------------------------------------------------------------------- 1 | # tag::PIXEL[] 2 | >>> class Pixel: 3 | ... __slots__ = ('x', 'y') # <1> 4 | ... 5 | >>> p = Pixel() # <2> 6 | >>> p.__dict__ # <3> 7 | Traceback (most recent call last): 8 | ... 9 | AttributeError: 'Pixel' object has no attribute '__dict__' 10 | >>> p.x = 10 # <4> 11 | >>> p.y = 20 12 | >>> p.color = 'red' # <5> 13 | Traceback (most recent call last): 14 | ... 15 | AttributeError: 'Pixel' object has no attribute 'color' 16 | 17 | # end::PIXEL[] 18 | 19 | # tag::OPEN_PIXEL[] 20 | >>> class OpenPixel(Pixel): # <1> 21 | ... pass 22 | ... 23 | >>> op = OpenPixel() 24 | >>> op.__dict__ # <2> 25 | {} 26 | >>> op.x = 8 # <3> 27 | >>> op.__dict__ # <4> 28 | {} 29 | >>> op.x # <5> 30 | 8 31 | >>> op.color = 'green' # <6> 32 | >>> op.__dict__ # <7> 33 | {'color': 'green'} 34 | 35 | # end::OPEN_PIXEL[] 36 | 37 | # tag::COLOR_PIXEL[] 38 | >>> class ColorPixel(Pixel): 39 | ... __slots__ = ('color',) # <1> 40 | >>> cp = ColorPixel() 41 | >>> cp.__dict__ # <2> 42 | Traceback (most recent call last): 43 | ... 44 | AttributeError: 'ColorPixel' object has no attribute '__dict__' 45 | >>> cp.x = 2 46 | >>> cp.color = 'blue' # <3> 47 | >>> cp.flavor = 'banana' 48 | Traceback (most recent call last): 49 | ... 50 | AttributeError: 'ColorPixel' object has no attribute 'flavor' 51 | 52 | # end::COLOR_PIXEL[] 53 | -------------------------------------------------------------------------------- /13-protocol-abc/README.rst: -------------------------------------------------------------------------------- 1 | Sample code for Chapter 11 - "Interfaces, protocols and ABCs" 2 | 3 | From the book "Fluent Python" by Luciano Ramalho (O'Reilly, 2015) 4 | http://shop.oreilly.com/product/0636920032519.do 5 | -------------------------------------------------------------------------------- /13-protocol-abc/bingo.py: -------------------------------------------------------------------------------- 1 | # tag::TOMBOLA_BINGO[] 2 | 3 | import random 4 | 5 | from tombola import Tombola 6 | 7 | 8 | class BingoCage(Tombola): # <1> 9 | 10 | def __init__(self, items): 11 | self._randomizer = random.SystemRandom() # <2> 12 | self._items = [] 13 | self.load(items) # <3> 14 | 15 | def load(self, items): 16 | self._items.extend(items) 17 | self._randomizer.shuffle(self._items) # <4> 18 | 19 | def pick(self): # <5> 20 | try: 21 | return self._items.pop() 22 | except IndexError: 23 | raise LookupError('pick from empty BingoCage') 24 | 25 | def __call__(self): # <6> 26 | self.pick() 27 | 28 | # end::TOMBOLA_BINGO[] 29 | -------------------------------------------------------------------------------- /13-protocol-abc/double/double_object.py: -------------------------------------------------------------------------------- 1 | def double(x: object) -> object: 2 | return x * 2 3 | -------------------------------------------------------------------------------- /13-protocol-abc/double/double_protocol.py: -------------------------------------------------------------------------------- 1 | from typing import TypeVar, Protocol 2 | 3 | T = TypeVar('T') # <1> 4 | 5 | class Repeatable(Protocol): 6 | def __mul__(self: T, repeat_count: int) -> T: ... # <2> 7 | 8 | RT = TypeVar('RT', bound=Repeatable) # <3> 9 | 10 | def double(x: RT) -> RT: # <4> 11 | return x * 2 12 | -------------------------------------------------------------------------------- /13-protocol-abc/double/double_sequence.py: -------------------------------------------------------------------------------- 1 | from collections import abc 2 | from typing import Any 3 | 4 | def double(x: abc.Sequence) -> Any: 5 | return x * 2 6 | 7 | -------------------------------------------------------------------------------- /13-protocol-abc/drum.py: -------------------------------------------------------------------------------- 1 | from random import shuffle 2 | 3 | from tombola import Tombola 4 | 5 | 6 | class TumblingDrum(Tombola): 7 | 8 | def __init__(self, iterable): 9 | self._balls = [] 10 | self.load(iterable) 11 | 12 | def load(self, iterable): 13 | self._balls.extend(iterable) 14 | shuffle(self._balls) 15 | 16 | def pick(self): 17 | return self._balls.pop() 18 | -------------------------------------------------------------------------------- /13-protocol-abc/frenchdeck2.py: -------------------------------------------------------------------------------- 1 | from collections import namedtuple, abc 2 | 3 | Card = namedtuple('Card', ['rank', 'suit']) 4 | 5 | class FrenchDeck2(abc.MutableSequence): 6 | ranks = [str(n) for n in range(2, 11)] + list('JQKA') 7 | suits = 'spades diamonds clubs hearts'.split() 8 | 9 | def __init__(self): 10 | self._cards = [Card(rank, suit) for suit in self.suits 11 | for rank in self.ranks] 12 | 13 | def __len__(self): 14 | return len(self._cards) 15 | 16 | def __getitem__(self, position): 17 | return self._cards[position] 18 | 19 | def __setitem__(self, position, value): # <1> 20 | self._cards[position] = value 21 | 22 | def __delitem__(self, position): # <2> 23 | del self._cards[position] 24 | 25 | def insert(self, position, value): # <3> 26 | self._cards.insert(position, value) 27 | -------------------------------------------------------------------------------- /13-protocol-abc/lotto.py: -------------------------------------------------------------------------------- 1 | # tag::LOTTERY_BLOWER[] 2 | 3 | import random 4 | 5 | from tombola import Tombola 6 | 7 | 8 | class LottoBlower(Tombola): 9 | 10 | def __init__(self, iterable): 11 | self._balls = list(iterable) # <1> 12 | 13 | def load(self, iterable): 14 | self._balls.extend(iterable) 15 | 16 | def pick(self): 17 | try: 18 | position = random.randrange(len(self._balls)) # <2> 19 | except ValueError: 20 | raise LookupError('pick from empty LottoBlower') 21 | return self._balls.pop(position) # <3> 22 | 23 | def loaded(self): # <4> 24 | return bool(self._balls) 25 | 26 | def inspect(self): # <5> 27 | return tuple(self._balls) 28 | 29 | 30 | # end::LOTTERY_BLOWER[] 31 | -------------------------------------------------------------------------------- /13-protocol-abc/tombola.py: -------------------------------------------------------------------------------- 1 | # tag::TOMBOLA_ABC[] 2 | 3 | import abc 4 | 5 | class Tombola(abc.ABC): # <1> 6 | 7 | @abc.abstractmethod 8 | def load(self, iterable): # <2> 9 | """Add items from an iterable.""" 10 | 11 | @abc.abstractmethod 12 | def pick(self): # <3> 13 | """Remove item at random, returning it. 14 | 15 | This method should raise `LookupError` when the instance is empty. 16 | """ 17 | 18 | def loaded(self): # <4> 19 | """Return `True` if there's at least 1 item, `False` otherwise.""" 20 | return bool(self.inspect()) # <5> 21 | 22 | def inspect(self): 23 | """Return a sorted tuple with the items currently inside.""" 24 | items = [] 25 | while True: # <6> 26 | try: 27 | items.append(self.pick()) 28 | except LookupError: 29 | break 30 | self.load(items) # <7> 31 | return tuple(items) 32 | 33 | 34 | # end::TOMBOLA_ABC[] 35 | -------------------------------------------------------------------------------- /13-protocol-abc/tombola_runner.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import doctest 4 | 5 | from tombola import Tombola 6 | 7 | # modules to test 8 | import bingo, lotto, tombolist, drum # <1> 9 | 10 | TEST_FILE = 'tombola_tests.rst' 11 | TEST_MSG = '{0:16} {1.attempted:2} tests, {1.failed:2} failed - {2}' 12 | 13 | 14 | def main(argv): 15 | verbose = '-v' in argv 16 | real_subclasses = Tombola.__subclasses__() # <2> 17 | virtual_subclasses = [tombolist.TomboList] # <3> 18 | for cls in real_subclasses + virtual_subclasses: # <4> 19 | test(cls, verbose) 20 | 21 | 22 | def test(cls, verbose=False): 23 | 24 | res = doctest.testfile( 25 | TEST_FILE, 26 | globs={'ConcreteTombola': cls}, # <5> 27 | verbose=verbose, 28 | optionflags=doctest.REPORT_ONLY_FIRST_FAILURE) 29 | tag = 'FAIL' if res.failed else 'OK' 30 | print(TEST_MSG.format(cls.__name__, res, tag)) # <6> 31 | 32 | 33 | if __name__ == '__main__': 34 | import sys 35 | main(sys.argv) 36 | -------------------------------------------------------------------------------- /13-protocol-abc/tombolist.py: -------------------------------------------------------------------------------- 1 | from random import randrange 2 | 3 | from tombola import Tombola 4 | 5 | @Tombola.register # <1> 6 | class TomboList(list): # <2> 7 | 8 | def pick(self): 9 | if self: # <3> 10 | position = randrange(len(self)) 11 | return self.pop(position) # <4> 12 | else: 13 | raise LookupError('pop from empty TomboList') 14 | 15 | load = list.extend # <5> 16 | 17 | def loaded(self): 18 | return bool(self) # <6> 19 | 20 | def inspect(self): 21 | return tuple(self) 22 | 23 | # Tombola.register(TomboList) # <7> 24 | -------------------------------------------------------------------------------- /13-protocol-abc/typing/randompick.py: -------------------------------------------------------------------------------- 1 | from typing import Protocol, runtime_checkable, Any 2 | 3 | @runtime_checkable 4 | class RandomPicker(Protocol): 5 | def pick(self) -> Any: ... 6 | -------------------------------------------------------------------------------- /13-protocol-abc/typing/randompick_test.py: -------------------------------------------------------------------------------- 1 | import random 2 | from typing import Any, Iterable, TYPE_CHECKING 3 | 4 | from randompick import RandomPicker # <1> 5 | 6 | class SimplePicker: # <2> 7 | def __init__(self, items: Iterable) -> None: 8 | self._items = list(items) 9 | random.shuffle(self._items) 10 | 11 | def pick(self) -> Any: # <3> 12 | return self._items.pop() 13 | 14 | def test_isinstance() -> None: # <4> 15 | popper: RandomPicker = SimplePicker([1]) # <5> 16 | assert isinstance(popper, RandomPicker) # <6> 17 | 18 | def test_item_type() -> None: # <7> 19 | items = [1, 2] 20 | popper = SimplePicker(items) 21 | item = popper.pick() 22 | assert item in items 23 | if TYPE_CHECKING: 24 | reveal_type(item) # <8> 25 | assert isinstance(item, int) 26 | -------------------------------------------------------------------------------- /13-protocol-abc/typing/randompickload.py: -------------------------------------------------------------------------------- 1 | from typing import Protocol, runtime_checkable 2 | from randompick import RandomPicker 3 | 4 | @runtime_checkable # <1> 5 | class LoadableRandomPicker(RandomPicker, Protocol): # <2> 6 | def load(self, Iterable) -> None: ... # <3> 7 | -------------------------------------------------------------------------------- /13-protocol-abc/typing/randompickload_test.py: -------------------------------------------------------------------------------- 1 | import random 2 | from typing import Any, Iterable 3 | 4 | from randompickload import LoadableRandomPicker 5 | 6 | class SimplePicker: 7 | def __init__(self, items: Iterable) -> None: 8 | self._items = list(items) 9 | random.shuffle(self._items) 10 | 11 | def pick(self) -> Any: 12 | return self._items.pop() 13 | 14 | class LoadablePicker: # <1> 15 | def __init__(self, items: Iterable) -> None: 16 | self.load(items) 17 | 18 | def pick(self) -> Any: # <2> 19 | return self._items.pop() 20 | 21 | def load(self, items: Iterable) -> Any: # <3> 22 | self._items = list(items) 23 | random.shuffle(self._items) 24 | 25 | def test_isinstance() -> None: # <4> 26 | popper = LoadablePicker([1]) 27 | assert isinstance(popper, LoadableRandomPicker) 28 | 29 | def test_isinstance_not() -> None: # <5> 30 | popper = SimplePicker([1]) 31 | assert not isinstance(popper, LoadableRandomPicker) 32 | 33 | -------------------------------------------------------------------------------- /13-protocol-abc/typing/vector2d_v5_test.py: -------------------------------------------------------------------------------- 1 | from vector2d_v5 import Vector2d 2 | from typing import SupportsComplex, SupportsAbs, TYPE_CHECKING 3 | 4 | import pytest 5 | 6 | 7 | def test_SupportsComplex_subclass() -> None: 8 | assert issubclass(Vector2d, SupportsComplex) 9 | 10 | def test_SupportsComplex_isinstance() -> None: 11 | v = Vector2d(3, 4) 12 | assert isinstance(v, SupportsComplex) 13 | c = complex(v) 14 | assert c == 3 + 4j 15 | 16 | def test_SupportsAbs_subclass() -> None: 17 | assert issubclass(Vector2d, SupportsAbs) 18 | 19 | def test_SupportsAbs_isinstance() -> None: 20 | v = Vector2d(3, 4) 21 | assert isinstance(v, SupportsAbs) 22 | r = abs(v) 23 | assert r == 5.0 24 | if TYPE_CHECKING: 25 | reveal_type(r) # Revealed type is 'builtins.float*' 26 | 27 | def magnitude(v: SupportsAbs) -> float: 28 | return abs(v) 29 | 30 | def test_SupportsAbs_Vector2d_argument() -> None: 31 | assert 5.0 == magnitude(Vector2d(3, 4)) 32 | 33 | def test_SupportsAbs_object_argument() -> None: 34 | with pytest.raises(TypeError): 35 | assert 5.0 == magnitude(object()) 36 | -------------------------------------------------------------------------------- /14-inheritance/README.rst: -------------------------------------------------------------------------------- 1 | Sample code for Chapter 14 - "Inheritance: for better or for worse" 2 | 3 | From the book "Fluent Python, Second Edition" by Luciano Ramalho (O'Reilly, 2021) 4 | https://learning.oreilly.com/library/view/fluent-python-2nd/9781492056348/ 5 | -------------------------------------------------------------------------------- /15-more-types/cafeteria/contravariant.py: -------------------------------------------------------------------------------- 1 | # tag::TRASH_TYPES[] 2 | from typing import TypeVar, Generic 3 | 4 | class Refuse: # <1> 5 | """Any refuse.""" 6 | 7 | class Biodegradable(Refuse): 8 | """Biodegradable refuse.""" 9 | 10 | class Compostable(Biodegradable): 11 | """Compostable refuse.""" 12 | 13 | T_contra = TypeVar('T_contra', contravariant=True) # <2> 14 | 15 | class TrashCan(Generic[T_contra]): # <3> 16 | def put(self, refuse: T_contra) -> None: 17 | """Store trash until dumped.""" 18 | 19 | def deploy(trash_can: TrashCan[Biodegradable]): 20 | """Deploy a trash can for biodegradable refuse.""" 21 | # end::TRASH_TYPES[] 22 | 23 | 24 | ################################################ contravariant trash can 25 | 26 | 27 | # tag::DEPLOY_TRASH_CANS[] 28 | bio_can: TrashCan[Biodegradable] = TrashCan() 29 | deploy(bio_can) 30 | 31 | trash_can: TrashCan[Refuse] = TrashCan() 32 | deploy(trash_can) 33 | # end::DEPLOY_TRASH_CANS[] 34 | 35 | 36 | ################################################ more specific trash can 37 | 38 | # tag::DEPLOY_NOT_VALID[] 39 | compost_can: TrashCan[Compostable] = TrashCan() 40 | deploy(compost_can) 41 | ## mypy: Argument 1 to "deploy" has 42 | ## incompatible type "TrashCan[Compostable]" 43 | ## expected "TrashCan[Biodegradable]" 44 | # end::DEPLOY_NOT_VALID[] 45 | -------------------------------------------------------------------------------- /15-more-types/cast/empty.py: -------------------------------------------------------------------------------- 1 | # Mypy 0.812 can't spot this inevitable runtime IndexError 2 | print([][0]) -------------------------------------------------------------------------------- /15-more-types/cast/find.py: -------------------------------------------------------------------------------- 1 | # tag::CAST[] 2 | from typing import cast 3 | 4 | def find_first_str(a: list[object]) -> str: 5 | index = next(i for i, x in enumerate(a) if isinstance(x, str)) 6 | # We only get here if there's at least one string 7 | return cast(str, a[index]) 8 | # end::CAST[] 9 | 10 | 11 | from typing import TYPE_CHECKING 12 | 13 | l1 = [10, 20, 'thirty', 40] 14 | if TYPE_CHECKING: 15 | reveal_type(l1) 16 | 17 | print(find_first_str(l1)) 18 | 19 | l2 = [0, ()] 20 | try: 21 | find_first_str(l2) 22 | except StopIteration as e: 23 | print(repr(e)) 24 | -------------------------------------------------------------------------------- /15-more-types/cast/tcp_echo.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | from asyncio import StreamReader, StreamWriter 4 | 5 | # tag::CAST_IMPORTS[] 6 | from asyncio.trsock import TransportSocket 7 | from typing import cast 8 | # end::CAST_IMPORTS[] 9 | 10 | async def handle_echo(reader: StreamReader, writer: StreamWriter) -> None: 11 | data = await reader.read(100) 12 | message = data.decode() 13 | addr = writer.get_extra_info('peername') 14 | 15 | print(f"Received {message!r} from {addr!r}") 16 | 17 | print(f"Send: {message!r}") 18 | writer.write(data) 19 | await writer.drain() 20 | 21 | print("Close the connection") 22 | writer.close() 23 | 24 | async def main() -> None: 25 | server = await asyncio.start_server( 26 | handle_echo, '127.0.0.1', 8888) 27 | 28 | # tag::CAST_USE[] 29 | socket_list = cast(tuple[TransportSocket, ...], server.sockets) 30 | addr = socket_list[0].getsockname() 31 | # end::CAST_USE[] 32 | print(f'Serving on {addr}') 33 | 34 | async with server: 35 | await server.serve_forever() 36 | 37 | asyncio.run(main()) 38 | -------------------------------------------------------------------------------- /15-more-types/cast/tcp_echo_no_cast.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | from asyncio import StreamReader, StreamWriter 4 | 5 | async def handle_echo(reader: StreamReader, writer: StreamWriter) -> None: 6 | data = await reader.read(100) 7 | message = data.decode() 8 | addr = writer.get_extra_info('peername') 9 | 10 | print(f"Received {message!r} from {addr!r}") 11 | 12 | print(f"Send: {message!r}") 13 | writer.write(data) 14 | await writer.drain() 15 | 16 | print("Close the connection") 17 | writer.close() 18 | 19 | async def main() -> None: 20 | server = await asyncio.start_server( 21 | handle_echo, '127.0.0.1', 8888) 22 | 23 | addr = server.sockets[0].getsockname() 24 | print(f'Serving on {addr}') 25 | 26 | async with server: 27 | await server.serve_forever() 28 | 29 | asyncio.run(main()) 30 | -------------------------------------------------------------------------------- /15-more-types/clip_annot.py: -------------------------------------------------------------------------------- 1 | """ 2 | >>> clip('banana ', 6) 3 | 'banana' 4 | >>> clip('banana ', 7) 5 | 'banana' 6 | >>> clip('banana ', 5) 7 | 'banana' 8 | >>> clip('banana split', 6) 9 | 'banana' 10 | >>> clip('banana split', 7) 11 | 'banana' 12 | >>> clip('banana split', 10) 13 | 'banana' 14 | >>> clip('banana split', 11) 15 | 'banana' 16 | >>> clip('banana split', 12) 17 | 'banana split' 18 | """ 19 | 20 | # tag::CLIP_ANNOT[] 21 | def clip(text: str, max_len: int = 80) -> str: 22 | """Return new ``str`` clipped at last space before or after ``max_len``. 23 | Return full ``text`` if no space found. 24 | """ 25 | end = None 26 | if len(text) > max_len: 27 | space_before = text.rfind(' ', 0, max_len) 28 | if space_before >= 0: 29 | end = space_before 30 | else: 31 | space_after = text.rfind(' ', max_len) 32 | if space_after >= 0: 33 | end = space_after 34 | if end is None: 35 | end = len(text) 36 | return text[:end].rstrip() 37 | 38 | # end::CLIP_ANNOT[] 39 | -------------------------------------------------------------------------------- /15-more-types/clip_annot_demo.py: -------------------------------------------------------------------------------- 1 | from clip_annot_post import clip 2 | 3 | print(clip.__annotations__) 4 | -------------------------------------------------------------------------------- /15-more-types/clip_annot_post.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | """ 4 | >>> clip('banana ', 6) 5 | 'banana' 6 | >>> clip('banana ', 7) 7 | 'banana' 8 | >>> clip('banana ', 5) 9 | 'banana' 10 | >>> clip('banana split', 6) 11 | 'banana' 12 | >>> clip('banana split', 7) 13 | 'banana' 14 | >>> clip('banana split', 10) 15 | 'banana' 16 | >>> clip('banana split', 11) 17 | 'banana' 18 | >>> clip('banana split', 12) 19 | 'banana split' 20 | """ 21 | 22 | # tag::CLIP_ANNOT[] 23 | def clip(text: str, max_len: int = 80) -> str: 24 | """Return new ``str`` clipped at last space before or after ``max_len``. 25 | Return full ``text`` if no space found. 26 | """ 27 | end = None 28 | if len(text) > max_len: 29 | space_before = text.rfind(' ', 0, max_len) 30 | if space_before >= 0: 31 | end = space_before 32 | else: 33 | space_after = text.rfind(' ', max_len) 34 | if space_after >= 0: 35 | end = space_after 36 | if end is None: 37 | end = len(text) 38 | return text[:end].rstrip() 39 | 40 | # end::CLIP_ANNOT[] 41 | -------------------------------------------------------------------------------- /15-more-types/collections_variance.py: -------------------------------------------------------------------------------- 1 | from collections.abc import Collection, Sequence 2 | 3 | col_int: Collection[int] 4 | 5 | seq_int: Sequence[int] = (1, 2, 3) 6 | 7 | ## Incompatible types in assignment 8 | ## expression has type "Collection[int]" 9 | ## variable has type "Sequence[int]" 10 | # seq_int = col_int 11 | 12 | col_int = seq_int 13 | 14 | ## List item 0 has incompatible type "float" 15 | ## expected "int" 16 | # col_int = [1.1] 17 | -------------------------------------------------------------------------------- /15-more-types/lotto/generic_lotto.py: -------------------------------------------------------------------------------- 1 | import random 2 | 3 | from collections.abc import Iterable 4 | from typing import TypeVar, Generic 5 | 6 | from tombola import Tombola 7 | 8 | T = TypeVar('T') 9 | 10 | class LottoBlower(Tombola, Generic[T]): # <1> 11 | 12 | def __init__(self, items: Iterable[T]) -> None: # <2> 13 | self._balls = list[T](items) 14 | 15 | def load(self, items: Iterable[T]) -> None: # <3> 16 | self._balls.extend(items) 17 | 18 | def pick(self) -> T: # <4> 19 | try: 20 | position = random.randrange(len(self._balls)) 21 | except ValueError: 22 | raise LookupError('pick from empty LottoBlower') 23 | return self._balls.pop(position) 24 | 25 | def loaded(self) -> bool: # <5> 26 | return bool(self._balls) 27 | 28 | def inspect(self) -> tuple[T, ...]: # <6> 29 | return tuple(self._balls) 30 | -------------------------------------------------------------------------------- /15-more-types/lotto/generic_lotto_demo.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from typing import TYPE_CHECKING 4 | 5 | # tag::LOTTO_USE[] 6 | from generic_lotto import LottoBlower 7 | 8 | machine = LottoBlower[int](range(1, 11)) # <1> 9 | 10 | first = machine.pick() # <2> 11 | remain = machine.inspect() # <3> 12 | # end::LOTTO_USE[] 13 | 14 | expected = set(i for i in range(1, 11) if i != first) 15 | 16 | assert set(remain) == expected 17 | 18 | print('picked:', first) 19 | print('remain:', remain) 20 | 21 | if TYPE_CHECKING: 22 | reveal_type(first) 23 | # Revealed type is 'builtins.int*' 24 | if TYPE_CHECKING: 25 | reveal_type(remain) 26 | # Revealed type is 'builtins.tuple[builtins.int*]' 27 | 28 | 29 | -------------------------------------------------------------------------------- /15-more-types/lotto/generic_lotto_errors.py: -------------------------------------------------------------------------------- 1 | from generic_lotto import LottoBlower 2 | 3 | machine = LottoBlower[int]([1, .2]) 4 | ## error: List item 1 has incompatible type "float"; # <1> 5 | ## expected "int" 6 | 7 | machine = LottoBlower[int](range(1, 11)) 8 | 9 | machine.load('ABC') 10 | ## error: Argument 1 to "load" of "LottoBlower" # <2> 11 | ## has incompatible type "str"; 12 | ## expected "Iterable[int]" 13 | ## note: Following member(s) of "str" have conflicts: 14 | ## note: Expected: 15 | ## note: def __iter__(self) -> Iterator[int] 16 | ## note: Got: 17 | ## note: def __iter__(self) -> Iterator[str] 18 | 19 | -------------------------------------------------------------------------------- /15-more-types/lotto/tombola.py: -------------------------------------------------------------------------------- 1 | # tag::TOMBOLA_ABC[] 2 | 3 | import abc 4 | 5 | class Tombola(abc.ABC): # <1> 6 | 7 | @abc.abstractmethod 8 | def load(self, iterable): # <2> 9 | """Add items from an iterable.""" 10 | 11 | @abc.abstractmethod 12 | def pick(self): # <3> 13 | """Remove item at random, returning it. 14 | 15 | This method should raise `LookupError` when the instance is empty. 16 | """ 17 | 18 | def loaded(self): # <4> 19 | """Return `True` if there's at least 1 item, `False` otherwise.""" 20 | return bool(self.inspect()) # <5> 21 | 22 | def inspect(self): 23 | """Return a sorted tuple with the items currently inside.""" 24 | items = [] 25 | while True: # <6> 26 | try: 27 | items.append(self.pick()) 28 | except LookupError: 29 | break 30 | self.load(items) # <7> 31 | return tuple(items) 32 | 33 | 34 | # end::TOMBOLA_ABC[] 35 | -------------------------------------------------------------------------------- /15-more-types/mysum.py: -------------------------------------------------------------------------------- 1 | import functools 2 | import operator 3 | from collections.abc import Iterable 4 | from typing import overload, Union, TypeVar 5 | 6 | T = TypeVar('T') 7 | S = TypeVar('S') # <1> 8 | 9 | @overload 10 | def sum(it: Iterable[T]) -> Union[T, int]: ... # <2> 11 | @overload 12 | def sum(it: Iterable[T], /, start: S) -> Union[T, S]: ... # <3> 13 | def sum(it, /, start=0): # <4> 14 | return functools.reduce(operator.add, it, start) 15 | -------------------------------------------------------------------------------- /15-more-types/petbox/petbox.py: -------------------------------------------------------------------------------- 1 | """ 2 | Example adapted from `Atomic Kotlin` by Bruce Eckel & Svetlana Isakova, 3 | chapter `Creating Generics`, section `Variance`. 4 | """ 5 | 6 | from typing import TypeVar, Generic, Any 7 | 8 | 9 | class Pet: 10 | """Domestic animal kept for companionship.""" 11 | 12 | 13 | class Cat(Pet): 14 | """Felis catus""" 15 | 16 | 17 | class Siamese(Cat): 18 | """Cat breed from Thailand""" 19 | 20 | 21 | T = TypeVar('T') 22 | 23 | 24 | class Box(Generic[T]): 25 | def put(self, item: T) -> None: 26 | self.contents = item 27 | 28 | def get(self) -> T: 29 | return self.contents 30 | 31 | 32 | T_co = TypeVar('T_co', covariant=True) 33 | 34 | 35 | class OutBox(Generic[T_co]): 36 | def __init__(self, contents: Any): 37 | self.contents = contents 38 | 39 | def get(self) -> Any: 40 | return self.contents 41 | 42 | 43 | T_contra = TypeVar('T_contra', contravariant=True) 44 | 45 | 46 | class InBox(Generic[T_contra]): 47 | def put(self, item: T) -> None: 48 | self.contents = item 49 | -------------------------------------------------------------------------------- /15-more-types/petbox/petbox_demo.py: -------------------------------------------------------------------------------- 1 | """ 2 | Example adapted from `Atomic Kotlin` by Bruce Eckel & Svetlana Isakova, 3 | chapter `Creating Generics`, section `Variance`. 4 | """ 5 | 6 | from typing import TYPE_CHECKING 7 | 8 | from petbox import * 9 | 10 | 11 | cat_box: Box[Cat] = Box() 12 | 13 | si = Siamese() 14 | 15 | cat_box.put(si) 16 | 17 | animal = cat_box.get() 18 | 19 | # if TYPE_CHECKING: 20 | # reveal_type(animal) # Revealed: petbox.Cat* 21 | 22 | 23 | ################### Covariance 24 | 25 | out_box: OutBox[Cat] = OutBox(Cat()) 26 | 27 | out_box_si: OutBox[Siamese] = OutBox(Siamese()) 28 | 29 | out_box = out_box_si 30 | 31 | ## Incompatible types in assignment 32 | ## expression has type "OutBox[Cat]" 33 | ## variable has type "OutBox[Siamese]" 34 | # out_box_si = out_box 35 | 36 | ################### Contravariance 37 | 38 | in_box: InBox[Cat] = InBox() 39 | 40 | in_box_si: InBox[Siamese] = InBox() 41 | 42 | ## Incompatible types in assignment 43 | ## expression has type "InBox[Siamese]" 44 | ## variable has type "InBox[Cat]" 45 | # in_box = in_box_si 46 | 47 | in_box_si = in_box 48 | -------------------------------------------------------------------------------- /15-more-types/protocol/abs_demo.py: -------------------------------------------------------------------------------- 1 | import math 2 | from typing import NamedTuple, SupportsAbs 3 | 4 | class Vector2d(NamedTuple): 5 | x: float 6 | y: float 7 | 8 | def __abs__(self) -> float: # <1> 9 | return math.hypot(self.x, self.y) 10 | 11 | def is_unit(v: SupportsAbs[float]) -> bool: # <2> 12 | """'True' if the magnitude of 'v' is close to 1.""" 13 | return math.isclose(abs(v), 1.0) # <3> 14 | 15 | assert issubclass(Vector2d, SupportsAbs) # <4> 16 | 17 | v0 = Vector2d(0, 1) # <5> 18 | sqrt2 = math.sqrt(2) 19 | v1 = Vector2d(sqrt2 / 2, sqrt2 / 2) 20 | v2 = Vector2d(1, 1) 21 | v3 = complex(.5, math.sqrt(3) / 2) 22 | v4 = 1 # <6> 23 | 24 | assert is_unit(v0) 25 | assert is_unit(v1) 26 | assert not is_unit(v2) 27 | assert is_unit(v3) 28 | assert is_unit(v4) 29 | 30 | print('OK') 31 | -------------------------------------------------------------------------------- /15-more-types/protocol/random/erp.py: -------------------------------------------------------------------------------- 1 | import random 2 | from typing import TypeVar, Generic, List, Iterable 3 | 4 | 5 | T = TypeVar('T') 6 | 7 | 8 | class EnterpriserRandomPopper(Generic[T]): 9 | def __init__(self, items: Iterable[T]) -> None: 10 | self._items: List[T] = list(items) 11 | random.shuffle(self._items) 12 | 13 | def pop_random(self) -> T: 14 | return self._items.pop() 15 | -------------------------------------------------------------------------------- /15-more-types/protocol/random/erp_test.py: -------------------------------------------------------------------------------- 1 | from typing import TYPE_CHECKING 2 | 3 | from erp import EnterpriserRandomPopper 4 | import randompop 5 | 6 | 7 | def test_issubclass() -> None: 8 | assert issubclass(EnterpriserRandomPopper, randompop.RandomPopper) 9 | 10 | 11 | def test_isinstance_untyped_items_argument() -> None: 12 | items = [1, 2, 3] 13 | popper = EnterpriserRandomPopper(items) # [int] is not required 14 | if TYPE_CHECKING: 15 | reveal_type(popper) 16 | # Revealed type is 'erp.EnterpriserRandomPopper[builtins.int*]' 17 | assert isinstance(popper, randompop.RandomPopper) 18 | 19 | 20 | def test_isinstance_untyped_items_in_var_type() -> None: 21 | items = [1, 2, 3] 22 | popper: EnterpriserRandomPopper = EnterpriserRandomPopper[int](items) 23 | if TYPE_CHECKING: 24 | reveal_type(popper) 25 | # Revealed type is 'erp.EnterpriserRandomPopper[Any]' 26 | assert isinstance(popper, randompop.RandomPopper) 27 | 28 | 29 | def test_isinstance_item() -> None: 30 | items = [1, 2, 3] 31 | popper = EnterpriserRandomPopper[int](items) # [int] is not required 32 | popped = popper.pop_random() 33 | if TYPE_CHECKING: 34 | reveal_type(popped) 35 | # Revealed type is 'builtins.int*' 36 | assert isinstance(popped, int) 37 | -------------------------------------------------------------------------------- /15-more-types/protocol/random/generic_randompick.py: -------------------------------------------------------------------------------- 1 | from typing import Protocol, runtime_checkable, TypeVar 2 | 3 | T_co = TypeVar('T_co', covariant=True) # <1> 4 | 5 | @runtime_checkable 6 | class RandomPicker(Protocol[T_co]): # <2> 7 | def pick(self) -> T_co: ... # <3> 8 | -------------------------------------------------------------------------------- /15-more-types/protocol/random/generic_randompick_test.py: -------------------------------------------------------------------------------- 1 | import random 2 | from typing import Iterable, Generic, TypeVar, TYPE_CHECKING 3 | 4 | T_co = TypeVar('T_co', covariant=True) 5 | 6 | from generic_randompick import RandomPicker 7 | 8 | 9 | class LottoPicker(Generic[T_co]): 10 | def __init__(self, items: Iterable[T_co]) -> None: 11 | self._items = list(items) 12 | random.shuffle(self._items) 13 | 14 | def pick(self) -> T_co: 15 | return self._items.pop() 16 | 17 | 18 | def test_issubclass() -> None: 19 | assert issubclass(LottoPicker, RandomPicker) 20 | 21 | 22 | def test_isinstance() -> None: 23 | popper: RandomPicker = LottoPicker[int]([1]) 24 | if TYPE_CHECKING: 25 | reveal_type(popper) 26 | # Revealed type is '???' 27 | assert isinstance(popper, LottoPicker) 28 | 29 | 30 | def test_pick_type() -> None: 31 | balls = [1, 2, 3] 32 | popper = LottoPicker(balls) 33 | pick = popper.pick() 34 | assert pick in balls 35 | if TYPE_CHECKING: 36 | reveal_type(pick) 37 | # Revealed type is '???' -------------------------------------------------------------------------------- /15-more-types/protocol/random/randompop.py: -------------------------------------------------------------------------------- 1 | from typing import Protocol, runtime_checkable, Any 2 | 3 | 4 | @runtime_checkable 5 | class RandomPopper(Protocol): 6 | def pop_random(self) -> Any: ... 7 | -------------------------------------------------------------------------------- /15-more-types/protocol/random/randompop_test.py: -------------------------------------------------------------------------------- 1 | from randompop import RandomPopper 2 | import random 3 | from typing import Any, Iterable, TYPE_CHECKING 4 | 5 | 6 | class SimplePopper: 7 | def __init__(self, items: Iterable) -> None: 8 | self._items = list(items) 9 | random.shuffle(self._items) 10 | 11 | def pop_random(self) -> Any: 12 | return self._items.pop() 13 | 14 | 15 | def test_issubclass() -> None: 16 | assert issubclass(SimplePopper, RandomPopper) 17 | 18 | 19 | def test_isinstance() -> None: 20 | popper: RandomPopper = SimplePopper([1]) 21 | if TYPE_CHECKING: 22 | reveal_type(popper) 23 | # Revealed type is 'randompop.RandomPopper' 24 | assert isinstance(popper, RandomPopper) 25 | -------------------------------------------------------------------------------- /15-more-types/typeddict/books.py: -------------------------------------------------------------------------------- 1 | import json 2 | # tag::BOOKDICT[] 3 | from typing import TypedDict 4 | 5 | class BookDict(TypedDict): 6 | isbn: str 7 | title: str 8 | authors: list[str] 9 | pagecount: int 10 | # end::BOOKDICT[] 11 | 12 | # tag::TOXML[] 13 | AUTHOR_ELEMENT = '{}' 14 | 15 | def to_xml(book: BookDict) -> str: # <1> 16 | elements: list[str] = [] # <2> 17 | for key, value in book.items(): 18 | if isinstance(value, list): # <3> 19 | elements.extend( 20 | AUTHOR_ELEMENT.format(n) for n in value) # <4> 21 | else: 22 | tag = key.upper() 23 | elements.append(f'<{tag}>{value}') 24 | xml = '\n\t'.join(elements) 25 | return f'\n\t{xml}\n' 26 | # end::TOXML[] 27 | 28 | # tag::FROMJSON[] 29 | def from_json(data: str) -> BookDict: 30 | whatever: BookDict = json.loads(data) # <1> 31 | return whatever # <2> 32 | # end::FROMJSON[] 33 | -------------------------------------------------------------------------------- /15-more-types/typeddict/books_any.py: -------------------------------------------------------------------------------- 1 | # tag::BOOKDICT[] 2 | from typing import TypedDict, List 3 | import json 4 | 5 | class BookDict(TypedDict): 6 | isbn: str 7 | title: str 8 | authors: List[str] 9 | pagecount: int 10 | # end::BOOKDICT[] 11 | 12 | # tag::TOXML[] 13 | AUTHOR_ELEMENT = '{}' 14 | 15 | def to_xml(book: BookDict) -> str: # <1> 16 | elements: List[str] = [] # <2> 17 | for key, value in book.items(): 18 | if isinstance(value, list): # <3> 19 | elements.extend(AUTHOR_ELEMENT.format(n) 20 | for n in value) 21 | else: 22 | tag = key.upper() 23 | elements.append(f'<{tag}>{value}') 24 | xml = '\n\t'.join(elements) 25 | return f'\n\t{xml}\n' 26 | # end::TOXML[] 27 | 28 | # tag::FROMJSON[] 29 | def from_json(data: str) -> BookDict: 30 | whatever = json.loads(data) # <1> 31 | return whatever # <2> 32 | # end::FROMJSON[] 33 | -------------------------------------------------------------------------------- /15-more-types/typeddict/demo_books.py: -------------------------------------------------------------------------------- 1 | from books import BookDict 2 | from typing import TYPE_CHECKING 3 | 4 | def demo() -> None: # <1> 5 | book = BookDict( # <2> 6 | isbn='0134757599', 7 | title='Refactoring, 2e', 8 | authors=['Martin Fowler', 'Kent Beck'], 9 | pagecount=478 10 | ) 11 | authors = book['authors'] # <3> 12 | if TYPE_CHECKING: # <4> 13 | reveal_type(authors) # <5> 14 | authors = 'Bob' # <6> 15 | book['weight'] = 4.2 16 | del book['title'] 17 | 18 | 19 | if __name__ == '__main__': 20 | demo() 21 | -------------------------------------------------------------------------------- /15-more-types/typeddict/demo_not_book.py: -------------------------------------------------------------------------------- 1 | from books import to_xml, from_json 2 | from typing import TYPE_CHECKING 3 | 4 | def demo() -> None: 5 | NOT_BOOK_JSON = """ 6 | {"title": "Andromeda Strain", 7 | "flavor": "pistachio", 8 | "authors": true} 9 | """ 10 | not_book = from_json(NOT_BOOK_JSON) # <1> 11 | if TYPE_CHECKING: # <2> 12 | reveal_type(not_book) 13 | reveal_type(not_book['authors']) 14 | 15 | print(not_book) # <3> 16 | print(not_book['flavor']) # <4> 17 | 18 | xml = to_xml(not_book) # <5> 19 | print(xml) # <6> 20 | 21 | 22 | if __name__ == '__main__': 23 | demo() 24 | -------------------------------------------------------------------------------- /15-more-types/typeddict/test_books_check_fails.py: -------------------------------------------------------------------------------- 1 | from books import BookDict, to_xml 2 | 3 | XML_SAMPLE = """ 4 | 5 | \t0134757599 6 | \tRefactoring, 2e 7 | \tMartin Fowler 8 | \tKent Beck 9 | \t478 10 | 11 | """.strip() 12 | 13 | def test_3() -> None: 14 | xml = to_xml(BookDict(dict([ # Expected keyword arguments, {...}, or dict(...) in TypedDict constructor 15 | ('isbn', '0134757599'), 16 | ('title', 'Refactoring, 2e'), 17 | ('authors', ['Martin Fowler', 'Kent Beck']), 18 | ('pagecount', 478), 19 | ]))) 20 | assert xml == XML_SAMPLE 21 | -------------------------------------------------------------------------------- /16-op-overloading/README.rst: -------------------------------------------------------------------------------- 1 | Sample code for Chapter 13 - "Operator overloading: doing it right" 2 | 3 | From the book "Fluent Python" by Luciano Ramalho (O'Reilly, 2015) 4 | http://shop.oreilly.com/product/0636920032519.do 5 | -------------------------------------------------------------------------------- /16-op-overloading/bingo.py: -------------------------------------------------------------------------------- 1 | # BEGIN TOMBOLA_BINGO 2 | 3 | import random 4 | 5 | from tombola import Tombola 6 | 7 | 8 | class BingoCage(Tombola): # <1> 9 | 10 | def __init__(self, items): 11 | self._randomizer = random.SystemRandom() # <2> 12 | self._items = [] 13 | self.load(items) # <3> 14 | 15 | def load(self, items): 16 | self._items.extend(items) 17 | self._randomizer.shuffle(self._items) # <4> 18 | 19 | def pick(self): # <5> 20 | try: 21 | return self._items.pop() 22 | except IndexError: 23 | raise LookupError('pick from empty BingoCage') 24 | 25 | def __call__(self): # <7> 26 | self.pick() 27 | 28 | # END TOMBOLA_BINGO 29 | -------------------------------------------------------------------------------- /16-op-overloading/tombola.py: -------------------------------------------------------------------------------- 1 | # BEGIN TOMBOLA_ABC 2 | 3 | import abc 4 | 5 | class Tombola(abc.ABC): # <1> 6 | 7 | @abc.abstractmethod 8 | def load(self, iterable): # <2> 9 | """Add items from an iterable.""" 10 | 11 | @abc.abstractmethod 12 | def pick(self): # <3> 13 | """Remove item at random, returning it. 14 | 15 | This method should raise `LookupError` when the instance is empty. 16 | """ 17 | 18 | def loaded(self): # <4> 19 | """Return `True` if there's at least 1 item, `False` otherwise.""" 20 | return bool(self.inspect()) # <5> 21 | 22 | def inspect(self): 23 | """Return a sorted tuple with the items currently inside.""" 24 | items = [] 25 | while True: # <6> 26 | try: 27 | items.append(self.pick()) 28 | except LookupError: 29 | break 30 | self.load(items) # <7> 31 | return tuple(sorted(items)) 32 | 33 | # END TOMBOLA_ABC 34 | 35 | -------------------------------------------------------------------------------- /16-op-overloading/unary_plus_decimal.py: -------------------------------------------------------------------------------- 1 | """ 2 | # tag::UNARY_PLUS_DECIMAL[] 3 | 4 | >>> import decimal 5 | >>> ctx = decimal.getcontext() # <1> 6 | >>> ctx.prec = 40 # <2> 7 | >>> one_third = decimal.Decimal('1') / decimal.Decimal('3') # <3> 8 | >>> one_third # <4> 9 | Decimal('0.3333333333333333333333333333333333333333') 10 | >>> one_third == +one_third # <5> 11 | True 12 | >>> ctx.prec = 28 # <6> 13 | >>> one_third == +one_third # <7> 14 | False 15 | >>> +one_third # <8> 16 | Decimal('0.3333333333333333333333333333') 17 | 18 | # end::UNARY_PLUS_DECIMAL[] 19 | 20 | """ 21 | 22 | import decimal 23 | 24 | if __name__ == '__main__': 25 | 26 | with decimal.localcontext() as ctx: 27 | ctx.prec = 40 28 | print('precision:', ctx.prec) 29 | one_third = decimal.Decimal('1') / decimal.Decimal('3') 30 | print(' one_third:', one_third) 31 | print(' +one_third:', +one_third) 32 | 33 | print('precision:', decimal.getcontext().prec) 34 | print(' one_third:', one_third) 35 | print(' +one_third:', +one_third) 36 | -------------------------------------------------------------------------------- /17-it-generator/README.rst: -------------------------------------------------------------------------------- 1 | Sample code for Chapter 14 - "Iterables, iterators and generators" 2 | 3 | From the book "Fluent Python" by Luciano Ramalho (O'Reilly, 2015) 4 | http://shop.oreilly.com/product/0636920032519.do 5 | -------------------------------------------------------------------------------- /17-it-generator/aritprog.rst: -------------------------------------------------------------------------------- 1 | =========================================== 2 | Tests for arithmetic progression generators 3 | =========================================== 4 | 5 | Tests with built-in numeric types:: 6 | 7 | >>> ap = aritprog_gen(1, .5, 3) 8 | >>> list(ap) 9 | [1.0, 1.5, 2.0, 2.5] 10 | >>> ap = aritprog_gen(0, 1/3, 1) 11 | >>> list(ap) 12 | [0.0, 0.3333333333333333, 0.6666666666666666] 13 | 14 | 15 | Tests with standard library numeric types:: 16 | 17 | >>> from fractions import Fraction 18 | >>> ap = aritprog_gen(0, Fraction(1, 3), 1) 19 | >>> list(ap) 20 | [Fraction(0, 1), Fraction(1, 3), Fraction(2, 3)] 21 | >>> from decimal import Decimal 22 | >>> ap = aritprog_gen(0, Decimal('.1'), .3) 23 | >>> list(ap) 24 | [Decimal('0'), Decimal('0.1'), Decimal('0.2')] 25 | 26 | 27 | Test producing an empty series:: 28 | 29 | >>> ap = aritprog_gen(0, 1, 0) 30 | >>> list(ap) 31 | [] 32 | -------------------------------------------------------------------------------- /17-it-generator/aritprog_float_error.py: -------------------------------------------------------------------------------- 1 | """ 2 | Demonstrate difference between Arithmetic Progression calculated 3 | as a series of increments accumulating errors versus one addition 4 | and one multiplication. 5 | """ 6 | 7 | from fractions import Fraction 8 | from aritprog_v0 import ArithmeticProgression as APv0 9 | from aritprog_v1 import ArithmeticProgression as APv1 10 | 11 | if __name__ == '__main__': 12 | 13 | ap0 = iter(APv0(1, .1)) 14 | ap1 = iter(APv1(1, .1)) 15 | ap_frac = iter(APv1(Fraction(1, 1), Fraction(1, 10))) 16 | epsilon = 10**-10 17 | iteration = 0 18 | delta = next(ap0) - next(ap1) 19 | frac = next(ap_frac) 20 | while abs(delta) <= epsilon: 21 | delta = next(ap0) - next(ap1) 22 | frac = next(ap_frac) 23 | iteration += 1 24 | 25 | print('iteration: {}\tfraction: {}\tepsilon: {}\tdelta: {}'. 26 | format(iteration, frac, epsilon, delta)) 27 | -------------------------------------------------------------------------------- /17-it-generator/aritprog_runner.py: -------------------------------------------------------------------------------- 1 | import doctest 2 | import importlib 3 | import glob 4 | 5 | 6 | TARGET_GLOB = 'aritprog*.py' 7 | TEST_FILE = 'aritprog.rst' 8 | TEST_MSG = '{0:16} {1.attempted:2} tests, {1.failed:2} failed - {2}' 9 | 10 | 11 | def main(argv): 12 | verbose = '-v' in argv 13 | for module_file_name in sorted(glob.glob(TARGET_GLOB)): 14 | module_name = module_file_name.replace('.py', '') 15 | module = importlib.import_module(module_name) 16 | gen_factory = getattr(module, 'ArithmeticProgression', None) 17 | if gen_factory is None: 18 | gen_factory = getattr(module, 'aritprog_gen', None) 19 | if gen_factory is None: 20 | continue 21 | 22 | test(gen_factory, verbose) 23 | 24 | 25 | def test(gen_factory, verbose=False): 26 | res = doctest.testfile( 27 | TEST_FILE, 28 | globs={'aritprog_gen': gen_factory}, 29 | verbose=verbose, 30 | optionflags=doctest.REPORT_ONLY_FIRST_FAILURE) 31 | tag = 'FAIL' if res.failed else 'OK' 32 | print(TEST_MSG.format(gen_factory.__module__, res, tag)) 33 | 34 | 35 | if __name__ == '__main__': 36 | import sys 37 | main(sys.argv) 38 | -------------------------------------------------------------------------------- /17-it-generator/aritprog_v0.py: -------------------------------------------------------------------------------- 1 | """ 2 | Arithmetic progression class 3 | 4 | >>> ap = ArithmeticProgression(1, .5, 3) 5 | >>> list(ap) 6 | [1.0, 1.5, 2.0, 2.5] 7 | 8 | 9 | """ 10 | 11 | 12 | class ArithmeticProgression: 13 | 14 | def __init__(self, begin, step, end=None): 15 | self.begin = begin 16 | self.step = step 17 | self.end = end # None -> "infinite" series 18 | 19 | def __iter__(self): 20 | result_type = type(self.begin + self.step) 21 | result = result_type(self.begin) 22 | forever = self.end is None 23 | while forever or result < self.end: 24 | yield result 25 | result += self.step 26 | -------------------------------------------------------------------------------- /17-it-generator/aritprog_v2.py: -------------------------------------------------------------------------------- 1 | """ 2 | Arithmetic progression generator function:: 3 | 4 | >>> ap = aritprog_gen(1, .5, 3) 5 | >>> list(ap) 6 | [1.0, 1.5, 2.0, 2.5] 7 | >>> ap = aritprog_gen(0, 1/3, 1) 8 | >>> list(ap) 9 | [0.0, 0.3333333333333333, 0.6666666666666666] 10 | >>> from fractions import Fraction 11 | >>> ap = aritprog_gen(0, Fraction(1, 3), 1) 12 | >>> list(ap) 13 | [Fraction(0, 1), Fraction(1, 3), Fraction(2, 3)] 14 | >>> from decimal import Decimal 15 | >>> ap = aritprog_gen(0, Decimal('.1'), .3) 16 | >>> list(ap) 17 | [Decimal('0'), Decimal('0.1'), Decimal('0.2')] 18 | 19 | """ 20 | 21 | 22 | # tag::ARITPROG_GENFUNC[] 23 | def aritprog_gen(begin, step, end=None): 24 | result = type(begin + step)(begin) 25 | forever = end is None 26 | index = 0 27 | while forever or result < end: 28 | yield result 29 | index += 1 30 | result = begin + step * index 31 | # end::ARITPROG_GENFUNC[] 32 | -------------------------------------------------------------------------------- /17-it-generator/aritprog_v3.py: -------------------------------------------------------------------------------- 1 | # tag::ARITPROG_ITERTOOLS[] 2 | import itertools 3 | 4 | 5 | def aritprog_gen(begin, step, end=None): 6 | first = type(begin + step)(begin) 7 | ap_gen = itertools.count(first, step) 8 | if end is None: 9 | return ap_gen 10 | return itertools.takewhile(lambda n: n < end, ap_gen) 11 | # end::ARITPROG_ITERTOOLS[] 12 | -------------------------------------------------------------------------------- /17-it-generator/columnize_iter.py: -------------------------------------------------------------------------------- 1 | # tag::COLUMNIZE[] 2 | from collections.abc import Sequence, Iterator 3 | 4 | def columnize( 5 | sequence: Sequence[str], num_columns: int = 0 6 | ) -> Iterator[tuple[str, ...]]: # <1> 7 | if num_columns == 0: 8 | num_columns = round(len(sequence) ** 0.5) 9 | num_rows, remainder = divmod(len(sequence), num_columns) 10 | num_rows += bool(remainder) 11 | return (tuple(sequence[i::num_rows]) for i in range(num_rows)) # <2> 12 | # end::COLUMNIZE[] 13 | 14 | def demo() -> None: 15 | nato = ( 16 | 'Alfa Bravo Charlie Delta Echo Foxtrot Golf Hotel India' 17 | ' Juliett Kilo Lima Mike November Oscar Papa Quebec Romeo' 18 | ' Sierra Tango Uniform Victor Whiskey X-ray Yankee Zulu' 19 | ).split() 20 | 21 | for row in columnize(nato, 4): 22 | for word in row: 23 | print(f'{word:15}', end='') 24 | print() 25 | 26 | if __name__ == '__main__': 27 | demo() 28 | -------------------------------------------------------------------------------- /17-it-generator/coroaverager.py: -------------------------------------------------------------------------------- 1 | """ 2 | A coroutine to compute a running average 3 | 4 | # tag::CORO_AVERAGER_TEST[] 5 | >>> coro_avg = averager() # <1> 6 | >>> next(coro_avg) # <2> 7 | 0.0 8 | >>> coro_avg.send(10) # <3> 9 | 10.0 10 | >>> coro_avg.send(30) 11 | 20.0 12 | >>> coro_avg.send(5) 13 | 15.0 14 | 15 | # end::CORO_AVERAGER_TEST[] 16 | # tag::CORO_AVERAGER_TEST_CONT[] 17 | 18 | >>> coro_avg.send(20) # <1> 19 | 16.25 20 | >>> coro_avg.close() # <2> 21 | >>> coro_avg.close() # <3> 22 | >>> coro_avg.send(5) # <4> 23 | Traceback (most recent call last): 24 | ... 25 | StopIteration 26 | 27 | # end::CORO_AVERAGER_TEST_CONT[] 28 | 29 | """ 30 | 31 | # tag::CORO_AVERAGER[] 32 | from collections.abc import Generator 33 | 34 | def averager() -> Generator[float, float, None]: # <1> 35 | total = 0.0 36 | count = 0 37 | average = 0.0 38 | while True: # <2> 39 | term = yield average # <3> 40 | total += term 41 | count += 1 42 | average = total/count 43 | # end::CORO_AVERAGER[] 44 | -------------------------------------------------------------------------------- /17-it-generator/fibo_by_hand.py: -------------------------------------------------------------------------------- 1 | """ 2 | Fibonacci generator implemented "by hand" without generator objects 3 | 4 | >>> from itertools import islice 5 | >>> list(islice(Fibonacci(), 15)) 6 | [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377] 7 | 8 | """ 9 | 10 | 11 | # tag::FIBO_BY_HAND[] 12 | class Fibonacci: 13 | 14 | def __iter__(self): 15 | return FibonacciGenerator() 16 | 17 | 18 | class FibonacciGenerator: 19 | 20 | def __init__(self): 21 | self.a = 0 22 | self.b = 1 23 | 24 | def __next__(self): 25 | result = self.a 26 | self.a, self.b = self.b, self.a + self.b 27 | return result 28 | 29 | def __iter__(self): 30 | return self 31 | # end::FIBO_BY_HAND[] 32 | 33 | # for comparison, this is the usual implementation of a Fibonacci 34 | # generator in Python: 35 | 36 | 37 | def fibonacci(): 38 | a, b = 0, 1 39 | while True: 40 | yield a 41 | a, b = b, a + b 42 | 43 | 44 | if __name__ == '__main__': 45 | 46 | for x, y in zip(Fibonacci(), fibonacci()): 47 | assert x == y, f'{x} != {y}' 48 | print(x) 49 | if x > 10**10: 50 | break 51 | print('etc...') 52 | 53 | -------------------------------------------------------------------------------- /17-it-generator/fibo_gen.py: -------------------------------------------------------------------------------- 1 | from collections.abc import Iterator 2 | 3 | def fibonacci() -> Iterator[int]: 4 | a, b = 0, 1 5 | while True: 6 | yield a 7 | a, b = b, a + b 8 | -------------------------------------------------------------------------------- /17-it-generator/isis2json/README.rst: -------------------------------------------------------------------------------- 1 | isis2json.py 2 | ============ 3 | 4 | This directory contains a copy of the ``isis2json.py`` script, with 5 | minimal dependencies, just to allow the O'Reilly Atlas toolchain to 6 | render the listing of the script in appendix A of the book. 7 | 8 | If you want to use or contribute to this script, please get the full 9 | source code with all dependencies from the main ``isis2json`` 10 | repository: 11 | 12 | https://github.com/fluentpython/isis2json 13 | -------------------------------------------------------------------------------- /17-it-generator/iter_gen_type.py: -------------------------------------------------------------------------------- 1 | from collections.abc import Iterator 2 | from keyword import kwlist 3 | from typing import TYPE_CHECKING 4 | 5 | short_kw = (k for k in kwlist if len(k) < 5) # <1> 6 | 7 | if TYPE_CHECKING: 8 | reveal_type(short_kw) # <2> 9 | 10 | long_kw: Iterator[str] = (k for k in kwlist if len(k) >= 4) # <3> 11 | 12 | if TYPE_CHECKING: # <4> 13 | reveal_type(long_kw) 14 | -------------------------------------------------------------------------------- /17-it-generator/sentence.py: -------------------------------------------------------------------------------- 1 | """ 2 | Sentence: access words by index 3 | 4 | >>> text = 'To be, or not to be, that is the question' 5 | >>> s = Sentence(text) 6 | >>> len(s) 7 | 10 8 | >>> s[1], s[5] 9 | ('be', 'be') 10 | >>> s 11 | Sentence('To be, or no... the question') 12 | 13 | """ 14 | 15 | # tag::SENTENCE_SEQ[] 16 | import re 17 | import reprlib 18 | 19 | RE_WORD = re.compile(r'\w+') 20 | 21 | 22 | class Sentence: 23 | 24 | def __init__(self, text): 25 | self.text = text 26 | self.words = RE_WORD.findall(text) # <1> 27 | 28 | def __getitem__(self, index): 29 | return self.words[index] # <2> 30 | 31 | def __len__(self): # <3> 32 | return len(self.words) 33 | 34 | def __repr__(self): 35 | return 'Sentence(%s)' % reprlib.repr(self.text) # <4> 36 | 37 | # end::SENTENCE_SEQ[] 38 | -------------------------------------------------------------------------------- /17-it-generator/sentence_gen.py: -------------------------------------------------------------------------------- 1 | """ 2 | Sentence: iterate over words using a generator function 3 | """ 4 | 5 | # tag::SENTENCE_GEN[] 6 | import re 7 | import reprlib 8 | 9 | RE_WORD = re.compile(r'\w+') 10 | 11 | 12 | class Sentence: 13 | 14 | def __init__(self, text): 15 | self.text = text 16 | self.words = RE_WORD.findall(text) 17 | 18 | def __repr__(self): 19 | return 'Sentence(%s)' % reprlib.repr(self.text) 20 | 21 | def __iter__(self): 22 | for word in self.words: # <1> 23 | yield word # <2> 24 | # <3> 25 | 26 | # done! <4> 27 | 28 | # end::SENTENCE_GEN[] 29 | -------------------------------------------------------------------------------- /17-it-generator/sentence_gen2.py: -------------------------------------------------------------------------------- 1 | """ 2 | Sentence: iterate over words using a generator function 3 | """ 4 | 5 | # tag::SENTENCE_GEN2[] 6 | import re 7 | import reprlib 8 | 9 | RE_WORD = re.compile(r'\w+') 10 | 11 | 12 | class Sentence: 13 | 14 | def __init__(self, text): 15 | self.text = text # <1> 16 | 17 | def __repr__(self): 18 | return f'Sentence({reprlib.repr(self.text)})' 19 | 20 | def __iter__(self): 21 | for match in RE_WORD.finditer(self.text): # <2> 22 | yield match.group() # <3> 23 | 24 | # end::SENTENCE_GEN2[] 25 | -------------------------------------------------------------------------------- /17-it-generator/sentence_genexp.py: -------------------------------------------------------------------------------- 1 | """ 2 | Sentence: iterate over words using a generator expression 3 | """ 4 | 5 | # tag::SENTENCE_GENEXP[] 6 | import re 7 | import reprlib 8 | 9 | RE_WORD = re.compile(r'\w+') 10 | 11 | 12 | class Sentence: 13 | 14 | def __init__(self, text): 15 | self.text = text 16 | 17 | def __repr__(self): 18 | return f'Sentence({reprlib.repr(self.text)})' 19 | 20 | def __iter__(self): 21 | return (match.group() for match in RE_WORD.finditer(self.text)) 22 | # end::SENTENCE_GENEXP[] 23 | 24 | 25 | def main(): 26 | import sys 27 | import warnings 28 | try: 29 | filename = sys.argv[1] 30 | word_number = int(sys.argv[2]) 31 | except (IndexError, ValueError): 32 | print(f'Usage: {sys.argv[0]} ') 33 | sys.exit(2) # command line usage error 34 | with open(filename, 'rt', encoding='utf-8') as text_file: 35 | s = Sentence(text_file.read()) 36 | for n, word in enumerate(s, 1): 37 | if n == word_number: 38 | print(word) 39 | break 40 | else: 41 | warnings.warn(f'last word is #{n}, {word!r}') 42 | 43 | if __name__ == '__main__': 44 | main() 45 | -------------------------------------------------------------------------------- /17-it-generator/sentence_iter2.py: -------------------------------------------------------------------------------- 1 | """ 2 | Sentence: iterate over words using the Iterator Pattern, take #2 3 | 4 | WARNING: the Iterator Pattern is much simpler in idiomatic Python; 5 | see: sentence_gen*.py. 6 | """ 7 | 8 | import re 9 | import reprlib 10 | 11 | RE_WORD = re.compile(r'\w+') 12 | 13 | 14 | class Sentence: 15 | 16 | def __init__(self, text): 17 | self.text = text 18 | 19 | def __repr__(self): 20 | return f'Sentence({reprlib.repr(self.text)})' 21 | 22 | def __iter__(self): 23 | word_iter = RE_WORD.finditer(self.text) # <1> 24 | return SentenceIter(word_iter) # <2> 25 | 26 | 27 | class SentenceIter: 28 | 29 | def __init__(self, word_iter): 30 | self.word_iter = word_iter # <3> 31 | 32 | def __next__(self): 33 | match = next(self.word_iter) # <4> 34 | return match.group() # <5> 35 | 36 | def __iter__(self): 37 | return self 38 | -------------------------------------------------------------------------------- /17-it-generator/sentence_runner.py: -------------------------------------------------------------------------------- 1 | import doctest 2 | import importlib 3 | import glob 4 | 5 | 6 | TARGET_GLOB = 'sentence*.py' 7 | TEST_FILE = 'sentence.rst' 8 | TEST_MSG = '{0:16} {1.attempted:2} tests, {1.failed:2} failed - {2}' 9 | 10 | 11 | def main(argv): 12 | verbose = '-v' in argv 13 | for module_file_name in sorted(glob.glob(TARGET_GLOB)): 14 | module_name = module_file_name.replace('.py', '') 15 | module = importlib.import_module(module_name) 16 | try: 17 | cls = getattr(module, 'Sentence') 18 | except AttributeError: 19 | continue 20 | test(cls, verbose) 21 | 22 | 23 | def test(cls, verbose=False): 24 | 25 | res = doctest.testfile( 26 | TEST_FILE, 27 | globs={'Sentence': cls}, 28 | verbose=verbose, 29 | optionflags=doctest.REPORT_ONLY_FIRST_FAILURE) 30 | tag = 'FAIL' if res.failed else 'OK' 31 | print(TEST_MSG.format(cls.__module__, res, tag)) 32 | 33 | 34 | if __name__ == '__main__': 35 | import sys 36 | main(sys.argv) 37 | -------------------------------------------------------------------------------- /17-it-generator/tree/4steps/tree_step0.py: -------------------------------------------------------------------------------- 1 | def tree(cls): 2 | yield cls.__name__ 3 | 4 | 5 | if __name__ == '__main__': 6 | for cls_name in tree(BaseException): 7 | print(cls_name) 8 | -------------------------------------------------------------------------------- /17-it-generator/tree/4steps/tree_step1.py: -------------------------------------------------------------------------------- 1 | def tree(cls): 2 | yield cls.__name__, 0 3 | for sub_cls in cls.__subclasses__(): 4 | yield sub_cls.__name__, 1 5 | 6 | 7 | if __name__ == '__main__': 8 | for cls_name, level in tree(BaseException): 9 | indent = ' ' * 4 * level 10 | print(f'{indent}{cls_name}') 11 | -------------------------------------------------------------------------------- /17-it-generator/tree/4steps/tree_step2.py: -------------------------------------------------------------------------------- 1 | def tree(cls): 2 | yield cls.__name__, 0 3 | for sub_cls in cls.__subclasses__(): 4 | yield sub_cls.__name__, 1 5 | for sub_sub_cls in sub_cls.__subclasses__(): 6 | yield sub_sub_cls.__name__, 2 7 | 8 | 9 | if __name__ == '__main__': 10 | for cls_name, level in tree(BaseException): 11 | indent = ' ' * 4 * level 12 | print(f'{indent}{cls_name}') 13 | -------------------------------------------------------------------------------- /17-it-generator/tree/4steps/tree_step3.py: -------------------------------------------------------------------------------- 1 | def tree(cls, level=0): 2 | yield cls.__name__, level 3 | for sub_cls in cls.__subclasses__(): 4 | yield from tree(sub_cls, level + 1) 5 | 6 | 7 | if __name__ == '__main__': 8 | for cls_name, level in tree(BaseException): 9 | indent = ' ' * 4 * level 10 | print(f'{indent}{cls_name}') 11 | -------------------------------------------------------------------------------- /17-it-generator/tree/extra/drawtree.py: -------------------------------------------------------------------------------- 1 | from tree import tree 2 | 3 | SP = '\N{SPACE}' 4 | HLIN = '\N{BOX DRAWINGS LIGHT HORIZONTAL}' * 2 + SP # ── 5 | VLIN = '\N{BOX DRAWINGS LIGHT VERTICAL}' + SP * 3 # │ 6 | TEE = '\N{BOX DRAWINGS LIGHT VERTICAL AND RIGHT}' + HLIN # ├── 7 | ELBOW = '\N{BOX DRAWINGS LIGHT UP AND RIGHT}' + HLIN # └── 8 | 9 | 10 | def subclasses(cls): 11 | try: 12 | return cls.__subclasses__() 13 | except TypeError: # handle the `type` type 14 | return cls.__subclasses__(cls) 15 | 16 | 17 | def tree(cls, level=0, last_sibling=True): 18 | yield cls, level, last_sibling 19 | children = subclasses(cls) 20 | if children: 21 | last = children[-1] 22 | for child in children: 23 | yield from tree(child, level+1, child is last) 24 | 25 | 26 | def render_lines(tree_iter): 27 | cls, _, _ = next(tree_iter) 28 | yield cls.__name__ 29 | prefix = '' 30 | 31 | for cls, level, last in tree_iter: 32 | prefix = prefix[:4 * (level - 1)] 33 | prefix = prefix.replace(TEE, VLIN).replace(ELBOW, SP * 4) 34 | prefix += ELBOW if last else TEE 35 | yield prefix + cls.__name__ 36 | 37 | 38 | def draw(cls): 39 | for line in render_lines(tree(cls)): 40 | print(line) 41 | 42 | 43 | if __name__ == '__main__': 44 | draw(BaseException) 45 | -------------------------------------------------------------------------------- /17-it-generator/tree/extra/tree.py: -------------------------------------------------------------------------------- 1 | def tree(cls, level=0, last_sibling=True): 2 | yield cls, level, last_sibling 3 | subclasses = cls.__subclasses__() 4 | if subclasses: 5 | last = subclasses[-1] 6 | for sub_cls in subclasses: 7 | yield from tree(sub_cls, level+1, sub_cls is last) 8 | 9 | 10 | def display(cls): 11 | for cls, level, _ in tree(cls): 12 | indent = ' ' * 4 * level 13 | print(f'{indent}{cls.__name__}') 14 | 15 | 16 | if __name__ == '__main__': 17 | display(BaseException) 18 | -------------------------------------------------------------------------------- /17-it-generator/tree/step0/test_tree.py: -------------------------------------------------------------------------------- 1 | from tree import tree 2 | 3 | 4 | def test_1_level(): 5 | class One: pass 6 | expected = ['One'] 7 | result = list(tree(One)) 8 | assert expected == result 9 | -------------------------------------------------------------------------------- /17-it-generator/tree/step0/tree.py: -------------------------------------------------------------------------------- 1 | def tree(cls): 2 | yield cls.__name__ 3 | 4 | 5 | def display(cls): 6 | for cls_name in tree(cls): 7 | print(cls_name) 8 | 9 | 10 | if __name__ == '__main__': 11 | display(BaseException) 12 | -------------------------------------------------------------------------------- /17-it-generator/tree/step1/test_tree.py: -------------------------------------------------------------------------------- 1 | from tree import tree 2 | 3 | 4 | def test_1_level(): 5 | class One: pass 6 | expected = [('One', 0)] 7 | result = list(tree(One)) 8 | assert expected == result 9 | 10 | 11 | def test_2_levels_2_leaves(): 12 | class Branch: pass 13 | class Leaf1(Branch): pass 14 | class Leaf2(Branch): pass 15 | expected = [ 16 | ('Branch', 0), 17 | ('Leaf1', 1), 18 | ('Leaf2', 1), 19 | ] 20 | result = list(tree(Branch)) 21 | assert expected == result 22 | -------------------------------------------------------------------------------- /17-it-generator/tree/step1/tree.py: -------------------------------------------------------------------------------- 1 | def tree(cls): 2 | yield cls.__name__, 0 # <1> 3 | for sub_cls in cls.__subclasses__(): # <2> 4 | yield sub_cls.__name__, 1 # <3> 5 | 6 | 7 | def display(cls): 8 | for cls_name, level in tree(cls): 9 | indent = ' ' * 4 * level # <4> 10 | print(f'{indent}{cls_name}') 11 | 12 | 13 | if __name__ == '__main__': 14 | display(BaseException) -------------------------------------------------------------------------------- /17-it-generator/tree/step2/test_tree.py: -------------------------------------------------------------------------------- 1 | from tree import tree 2 | 3 | 4 | def test_1_level(): 5 | class One: pass 6 | expected = [('One', 0)] 7 | result = list(tree(One)) 8 | assert expected == result 9 | 10 | 11 | def test_2_levels_2_leaves(): 12 | class Branch: pass 13 | class Leaf1(Branch): pass 14 | class Leaf2(Branch): pass 15 | expected = [ 16 | ('Branch', 0), 17 | ('Leaf1', 1), 18 | ('Leaf2', 1), 19 | ] 20 | result = list(tree(Branch)) 21 | assert expected == result 22 | -------------------------------------------------------------------------------- /17-it-generator/tree/step2/tree.py: -------------------------------------------------------------------------------- 1 | def tree(cls): 2 | yield cls.__name__, 0 3 | yield from sub_tree(cls) # <1> 4 | 5 | 6 | def sub_tree(cls): 7 | for sub_cls in cls.__subclasses__(): 8 | yield sub_cls.__name__, 1 # <2> 9 | 10 | 11 | def display(cls): 12 | for cls_name, level in tree(cls): # <3> 13 | indent = ' ' * 4 * level 14 | print(f'{indent}{cls_name}') 15 | 16 | 17 | if __name__ == '__main__': 18 | display(BaseException) 19 | -------------------------------------------------------------------------------- /17-it-generator/tree/step3/test_tree.py: -------------------------------------------------------------------------------- 1 | from tree import tree 2 | 3 | 4 | def test_1_level(): 5 | class One: pass 6 | expected = [('One', 0)] 7 | result = list(tree(One)) 8 | assert expected == result 9 | 10 | 11 | def test_2_levels_2_leaves(): 12 | class Branch: pass 13 | class Leaf1(Branch): pass 14 | class Leaf2(Branch): pass 15 | expected = [ 16 | ('Branch', 0), 17 | ('Leaf1', 1), 18 | ('Leaf2', 1), 19 | ] 20 | result = list(tree(Branch)) 21 | assert expected == result 22 | 23 | 24 | def test_3_levels_1_leaf(): 25 | class X: pass 26 | class Y(X): pass 27 | class Z(Y): pass 28 | expected = [ 29 | ('X', 0), 30 | ('Y', 1), 31 | ('Z', 2), 32 | ] 33 | result = list(tree(X)) 34 | assert expected == result 35 | -------------------------------------------------------------------------------- /17-it-generator/tree/step3/tree.py: -------------------------------------------------------------------------------- 1 | def tree(cls): 2 | yield cls.__name__, 0 3 | yield from sub_tree(cls) 4 | 5 | 6 | def sub_tree(cls): 7 | for sub_cls in cls.__subclasses__(): 8 | yield sub_cls.__name__, 1 9 | for sub_sub_cls in sub_cls.__subclasses__(): 10 | yield sub_sub_cls.__name__, 2 11 | 12 | 13 | def display(cls): 14 | for cls_name, level in tree(cls): 15 | indent = ' ' * 4 * level 16 | print(f'{indent}{cls_name}') 17 | 18 | 19 | if __name__ == '__main__': 20 | display(BaseException) 21 | -------------------------------------------------------------------------------- /17-it-generator/tree/step4/tree.py: -------------------------------------------------------------------------------- 1 | def tree(cls): 2 | yield cls.__name__, 0 3 | yield from sub_tree(cls) 4 | 5 | 6 | # tag::SUB_TREE[] 7 | def sub_tree(cls): 8 | for sub_cls in cls.__subclasses__(): 9 | yield sub_cls.__name__, 1 10 | for sub_sub_cls in sub_cls.__subclasses__(): 11 | yield sub_sub_cls.__name__, 2 12 | for sub_sub_sub_cls in sub_sub_cls.__subclasses__(): 13 | yield sub_sub_sub_cls.__name__, 3 14 | # end::SUB_TREE[] 15 | 16 | 17 | def display(cls): 18 | for cls_name, level in tree(cls): 19 | indent = ' ' * 4 * level 20 | print(f'{indent}{cls_name}') 21 | 22 | 23 | if __name__ == '__main__': 24 | display(BaseException) 25 | -------------------------------------------------------------------------------- /17-it-generator/tree/step5/tree.py: -------------------------------------------------------------------------------- 1 | def tree(cls): 2 | yield cls.__name__, 0 3 | yield from sub_tree(cls, 1) 4 | 5 | 6 | def sub_tree(cls, level): 7 | for sub_cls in cls.__subclasses__(): 8 | yield sub_cls.__name__, level 9 | yield from sub_tree(sub_cls, level+1) 10 | 11 | 12 | def display(cls): 13 | for cls_name, level in tree(cls): 14 | indent = ' ' * 4 * level 15 | print(f'{indent}{cls_name}') 16 | 17 | 18 | if __name__ == '__main__': 19 | display(BaseException) 20 | -------------------------------------------------------------------------------- /17-it-generator/tree/step6/tree.py: -------------------------------------------------------------------------------- 1 | def tree(cls, level=0): 2 | yield cls.__name__, level 3 | for sub_cls in cls.__subclasses__(): 4 | yield from tree(sub_cls, level+1) 5 | 6 | 7 | def display(cls): 8 | for cls_name, level in tree(cls): 9 | indent = ' ' * 4 * level 10 | print(f'{indent}{cls_name}') 11 | 12 | 13 | if __name__ == '__main__': 14 | display(BaseException) 15 | -------------------------------------------------------------------------------- /17-it-generator/yield_delegate_fail.py: -------------------------------------------------------------------------------- 1 | """ Example from `Python: The Full Monty`__ -- A Tested Semantics for the 2 | Python Programming Language 3 | 4 | __ http://cs.brown.edu/~sk/Publications/Papers/Published/pmmwplck-python-full-monty/ 5 | 6 | "The following program, [...] seems to perform a simple abstraction over the 7 | process of yielding:" 8 | 9 | Citation: 10 | 11 | Joe Gibbs Politz, Alejandro Martinez, Matthew Milano, Sumner Warren, 12 | Daniel Patterson, Junsong Li, Anand Chitipothu, and Shriram Krishnamurthi. 13 | 2013. Python: the full monty. SIGPLAN Not. 48, 10 (October 2013), 217-232. 14 | DOI=10.1145/2544173.2509536 http://doi.acm.org/10.1145/2544173.2509536 15 | """ 16 | 17 | # tag::YIELD_DELEGATE_FAIL[] 18 | def f(): 19 | def do_yield(n): 20 | yield n 21 | x = 0 22 | while True: 23 | x += 1 24 | do_yield(x) 25 | # end::YIELD_DELEGATE_FAIL[] 26 | 27 | if __name__ == '__main__': 28 | print('Invoking f() results in an infinite loop') 29 | f() 30 | -------------------------------------------------------------------------------- /17-it-generator/yield_delegate_fix.py: -------------------------------------------------------------------------------- 1 | """ Example adapted from ``yield_delegate_fail.py`` 2 | 3 | The following program performs a simple abstraction over the process of 4 | yielding. 5 | 6 | """ 7 | 8 | # tag::YIELD_DELEGATE_FIX[] 9 | def f(): 10 | def do_yield(n): 11 | yield n 12 | x = 0 13 | while True: 14 | x += 1 15 | yield from do_yield(x) 16 | # end::YIELD_DELEGATE_FIX[] 17 | 18 | if __name__ == '__main__': 19 | print('Invoking f() now produces a generator') 20 | g = f() 21 | print(next(g)) 22 | print(next(g)) 23 | print(next(g)) 24 | 25 | -------------------------------------------------------------------------------- /18-with-match/README.rst: -------------------------------------------------------------------------------- 1 | Sample code for Chapter 15 - "Context managers and something else" 2 | 3 | From the book "Fluent Python" by Luciano Ramalho (O'Reilly, 2015) 4 | http://shop.oreilly.com/product/0636920032519.do 5 | -------------------------------------------------------------------------------- /18-with-match/lispy/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2010-2017 Peter Norvig 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /18-with-match/lispy/original/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2010-2017 Peter Norvig 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /18-with-match/lispy/original/README.md: -------------------------------------------------------------------------------- 1 | 2 | # Source of the originals 3 | 4 | * [lis.py](https://raw.githubusercontent.com/norvig/pytudes/705c0a335c1811a203e79587d7d41865cf7f41c7/py/lis.py) 5 | 6 | * [lispy.py](https://raw.githubusercontent.com/norvig/pytudes/705c0a335c1811a203e79587d7d41865cf7f41c7/py/lispy.py) 7 | 8 | * [lispytest.py](https://raw.githubusercontent.com/norvig/pytudes/705c0a335c1811a203e79587d7d41865cf7f41c7/py/lispytest.py) 9 | -------------------------------------------------------------------------------- /18-with-match/lispy/py3.10/quicksort.scm: -------------------------------------------------------------------------------- 1 | (define (quicksort lst) 2 | (if (null? lst) 3 | lst 4 | (begin 5 | (define pivot (car lst)) 6 | (define rest (cdr lst)) 7 | (append 8 | (quicksort 9 | (filter (lambda (x) (< x pivot)) rest)) 10 | (list pivot) 11 | (quicksort 12 | (filter (lambda (x) (>= x pivot)) rest))) 13 | ) 14 | ) 15 | ) 16 | (display 17 | (quicksort (list 2 1 6 3 4 0 8 9 7 5))) 18 | -------------------------------------------------------------------------------- /19-concurrency/primes/README.md: -------------------------------------------------------------------------------- 1 | # Race condition in orignal procs.py 2 | 3 | Thanks to reader Michael Albert who noticed the code I published during the Early Release had a race condition in `proc.py`. 4 | 5 | If you are curious, 6 | [this diff](https://github.com/fluentpython/example-code-2e/commit/2c1230579db99738a5e5e6802063bda585f6476d) 7 | shows the bug and how I fixed it—but note that I later refactored 8 | the example to delegate parts of `main` to the `start_jobs` and `report` functions. 9 | 10 | The problem was that I ended the `while` loop that retrieved the results when the `jobs` queue was empty. 11 | However, it was possible that the queue was empty but there were still processes working. 12 | If that happened, one or more results would not be reported. 13 | I did not notice the problem when I tested my original code, 14 | but Albert showed that adding a `sleep(1)` call before the `if jobs.empty()` line made the bug occur frequently. 15 | I adopted one of his solutions: have the `worker` function send back a `PrimeResult` with `n = 0` as a sentinel, 16 | to let the main loop know that the process had completed, ending the loop when all processes were done. -------------------------------------------------------------------------------- /19-concurrency/primes/primes.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import math 4 | 5 | PRIME_FIXTURE = [ 6 | (2, True), 7 | (142702110479723, True), 8 | (299593572317531, True), 9 | (3333333333333301, True), 10 | (3333333333333333, False), 11 | (3333335652092209, False), 12 | (4444444444444423, True), 13 | (4444444444444444, False), 14 | (4444444488888889, False), 15 | (5555553133149889, False), 16 | (5555555555555503, True), 17 | (5555555555555555, False), 18 | (6666666666666666, False), 19 | (6666666666666719, True), 20 | (6666667141414921, False), 21 | (7777777536340681, False), 22 | (7777777777777753, True), 23 | (7777777777777777, False), 24 | (9999999999999917, True), 25 | (9999999999999999, False), 26 | ] 27 | 28 | NUMBERS = [n for n, _ in PRIME_FIXTURE] 29 | 30 | # tag::IS_PRIME[] 31 | def is_prime(n: int) -> bool: 32 | if n < 2: 33 | return False 34 | if n == 2: 35 | return True 36 | if n % 2 == 0: 37 | return False 38 | 39 | root = math.isqrt(n) 40 | for i in range(3, root + 1, 2): 41 | if n % i == 0: 42 | return False 43 | return True 44 | # end::IS_PRIME[] 45 | 46 | if __name__ == '__main__': 47 | 48 | for n, prime in PRIME_FIXTURE: 49 | prime_res = is_prime(n) 50 | assert prime_res == prime 51 | print(n, prime) 52 | -------------------------------------------------------------------------------- /19-concurrency/primes/run_procs.sh: -------------------------------------------------------------------------------- 1 | #/bin/bash 2 | for i in {1..20}; do echo -n $i; python3 procs.py $i | tail -1; done 3 | -------------------------------------------------------------------------------- /19-concurrency/primes/sequential.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | """ 4 | sequential.py: baseline for comparing sequential, multiprocessing, 5 | and threading code for CPU-intensive work. 6 | """ 7 | 8 | from time import perf_counter 9 | from typing import NamedTuple 10 | 11 | from primes import is_prime, NUMBERS 12 | 13 | class Result(NamedTuple): # <1> 14 | prime: bool 15 | elapsed: float 16 | 17 | def check(n: int) -> Result: # <2> 18 | t0 = perf_counter() 19 | prime = is_prime(n) 20 | return Result(prime, perf_counter() - t0) 21 | 22 | def main() -> None: 23 | print(f'Checking {len(NUMBERS)} numbers sequentially:') 24 | t0 = perf_counter() 25 | for n in NUMBERS: # <3> 26 | prime, elapsed = check(n) 27 | label = 'P' if prime else ' ' 28 | print(f'{n:16} {label} {elapsed:9.6f}s') 29 | 30 | elapsed = perf_counter() - t0 # <4> 31 | print(f'Total time: {elapsed:.2f}s') 32 | 33 | if __name__ == '__main__': 34 | main() 35 | -------------------------------------------------------------------------------- /19-concurrency/primes/spinner_prime_async_broken.py: -------------------------------------------------------------------------------- 1 | # spinner_async_experiment.py 2 | 3 | # credits: Example by Luciano Ramalho inspired by 4 | # Michele Simionato's multiprocessing example in the python-list: 5 | # https://mail.python.org/pipermail/python-list/2009-February/675659.html 6 | 7 | import asyncio 8 | import itertools 9 | 10 | import primes 11 | 12 | async def spin(msg: str) -> None: 13 | for char in itertools.cycle(r'\|/-'): 14 | status = f'\r{char} {msg}' 15 | print(status, flush=True, end='') 16 | try: 17 | await asyncio.sleep(.1) 18 | except asyncio.CancelledError: 19 | break 20 | print('THIS WILL NEVER BE OUTPUT') 21 | 22 | async def check(n: int) -> int: 23 | return primes.is_prime(n) # <4> 24 | 25 | async def supervisor(n: int) -> int: 26 | spinner = asyncio.create_task(spin('thinking!')) # <1> 27 | print('spinner object:', spinner) # <2> 28 | result = await check(n) # <3> 29 | spinner.cancel() # <5> 30 | return result 31 | # end::SPINNER_ASYNC_EXPERIMENT[] 32 | 33 | def main() -> None: 34 | n = 5_000_111_000_222_021 35 | result = asyncio.run(supervisor(n)) 36 | msg = 'is' if result else 'is not' 37 | print(f'{n:,} {msg} prime') 38 | 39 | if __name__ == '__main__': 40 | main() 41 | -------------------------------------------------------------------------------- /19-concurrency/primes/spinner_prime_thread.py: -------------------------------------------------------------------------------- 1 | # spinner_prime_thread.py 2 | 3 | # credits: Adapted from Michele Simionato's 4 | # multiprocessing example in the python-list: 5 | # https://mail.python.org/pipermail/python-list/2009-February/675659.html 6 | 7 | import itertools 8 | from threading import Thread, Event 9 | 10 | from primes import is_prime 11 | 12 | def spin(msg: str, done: Event) -> None: # <1> 13 | for char in itertools.cycle(r'\|/-'): # <2> 14 | status = f'\r{char} {msg}' # <3> 15 | print(status, end='', flush=True) 16 | if done.wait(.1): # <4> 17 | break # <5> 18 | blanks = ' ' * len(status) 19 | print(f'\r{blanks}\r', end='') # <6> 20 | 21 | def check(n: int) -> int: 22 | return is_prime(n) 23 | 24 | def supervisor(n: int) -> int: # <1> 25 | done = Event() # <2> 26 | spinner = Thread(target=spin, 27 | args=('thinking!', done)) # <3> 28 | print(f'spinner object: {spinner}') # <4> 29 | spinner.start() # <5> 30 | result = check(n) # <6> 31 | done.set() # <7> 32 | spinner.join() # <8> 33 | return result 34 | 35 | def main() -> None: 36 | n = 5_000_111_000_222_021 37 | result = supervisor(n) # <9> 38 | msg = 'is' if result else 'is not' 39 | print(f'{n:,} {msg} prime') 40 | 41 | if __name__ == '__main__': 42 | main() 43 | -------------------------------------------------------------------------------- /19-concurrency/spinner_async.py: -------------------------------------------------------------------------------- 1 | # spinner_async.py 2 | 3 | # credits: Example by Luciano Ramalho inspired by 4 | # Michele Simionato's multiprocessing example in the python-list: 5 | # https://mail.python.org/pipermail/python-list/2009-February/675659.html 6 | 7 | # tag::SPINNER_ASYNC_TOP[] 8 | import asyncio 9 | import itertools 10 | 11 | async def spin(msg: str) -> None: # <1> 12 | for char in itertools.cycle(r'\|/-'): 13 | status = f'\r{char} {msg}' 14 | print(status, flush=True, end='') 15 | try: 16 | await asyncio.sleep(.1) # <2> 17 | except asyncio.CancelledError: # <3> 18 | break 19 | blanks = ' ' * len(status) 20 | print(f'\r{blanks}\r', end='') 21 | 22 | async def slow() -> int: 23 | await asyncio.sleep(3) # <4> 24 | return 42 25 | # end::SPINNER_ASYNC_TOP[] 26 | 27 | # tag::SPINNER_ASYNC_START[] 28 | def main() -> None: # <1> 29 | result = asyncio.run(supervisor()) # <2> 30 | print(f'Answer: {result}') 31 | 32 | async def supervisor() -> int: # <3> 33 | spinner = asyncio.create_task(spin('thinking!')) # <4> 34 | print(f'spinner object: {spinner}') # <5> 35 | result = await slow() # <6> 36 | spinner.cancel() # <7> 37 | return result 38 | 39 | if __name__ == '__main__': 40 | main() 41 | # end::SPINNER_ASYNC_START[] 42 | -------------------------------------------------------------------------------- /19-concurrency/spinner_async_experiment.py: -------------------------------------------------------------------------------- 1 | # spinner_async_experiment.py 2 | 3 | # credits: Example by Luciano Ramalho inspired by 4 | # Michele Simionato's multiprocessing example in the python-list: 5 | # https://mail.python.org/pipermail/python-list/2009-February/675659.html 6 | 7 | import asyncio 8 | import itertools 9 | import time 10 | 11 | async def spin(msg: str) -> None: 12 | for char in itertools.cycle(r'\|/-'): 13 | status = f'\r{char} {msg}' 14 | print(status, flush=True, end='') 15 | try: 16 | await asyncio.sleep(.1) 17 | except asyncio.CancelledError: 18 | break 19 | print('THIS WILL NEVER BE OUTPUT') 20 | 21 | # tag::SPINNER_ASYNC_EXPERIMENT[] 22 | async def slow() -> int: 23 | time.sleep(3) # <4> 24 | return 42 25 | 26 | async def supervisor() -> int: 27 | spinner = asyncio.create_task(spin('thinking!')) # <1> 28 | print(f'spinner object: {spinner}') # <2> 29 | result = await slow() # <3> 30 | spinner.cancel() # <5> 31 | return result 32 | # end::SPINNER_ASYNC_EXPERIMENT[] 33 | 34 | def main() -> None: 35 | result = asyncio.run(supervisor()) 36 | print(f'Answer: {result}') 37 | 38 | if __name__ == '__main__': 39 | main() 40 | -------------------------------------------------------------------------------- /19-concurrency/spinner_thread.py: -------------------------------------------------------------------------------- 1 | # spinner_thread.py 2 | 3 | # credits: Adapted from Michele Simionato's 4 | # multiprocessing example in the python-list: 5 | # https://mail.python.org/pipermail/python-list/2009-February/675659.html 6 | 7 | # tag::SPINNER_THREAD_TOP[] 8 | import itertools 9 | import time 10 | from threading import Thread, Event 11 | 12 | def spin(msg: str, done: Event) -> None: # <1> 13 | for char in itertools.cycle(r'\|/-'): # <2> 14 | status = f'\r{char} {msg}' # <3> 15 | print(status, end='', flush=True) 16 | if done.wait(.1): # <4> 17 | break # <5> 18 | blanks = ' ' * len(status) 19 | print(f'\r{blanks}\r', end='') # <6> 20 | 21 | def slow() -> int: 22 | time.sleep(3) # <7> 23 | return 42 24 | # end::SPINNER_THREAD_TOP[] 25 | 26 | # tag::SPINNER_THREAD_REST[] 27 | def supervisor() -> int: # <1> 28 | done = Event() # <2> 29 | spinner = Thread(target=spin, args=('thinking!', done)) # <3> 30 | print(f'spinner object: {spinner}') # <4> 31 | spinner.start() # <5> 32 | result = slow() # <6> 33 | done.set() # <7> 34 | spinner.join() # <8> 35 | return result 36 | 37 | def main() -> None: 38 | result = supervisor() # <9> 39 | print(f'Answer: {result}') 40 | 41 | if __name__ == '__main__': 42 | main() 43 | # end::SPINNER_THREAD_REST[] 44 | -------------------------------------------------------------------------------- /20-executors/demo_executor_map.py: -------------------------------------------------------------------------------- 1 | """ 2 | Experiment with ``ThreadPoolExecutor.map`` 3 | """ 4 | # tag::EXECUTOR_MAP[] 5 | from time import sleep, strftime 6 | from concurrent import futures 7 | 8 | def display(*args): # <1> 9 | print(strftime('[%H:%M:%S]'), end=' ') 10 | print(*args) 11 | 12 | def loiter(n): # <2> 13 | msg = '{}loiter({}): doing nothing for {}s...' 14 | display(msg.format('\t'*n, n, n)) 15 | sleep(n) 16 | msg = '{}loiter({}): done.' 17 | display(msg.format('\t'*n, n)) 18 | return n * 10 # <3> 19 | 20 | def main(): 21 | display('Script starting.') 22 | executor = futures.ThreadPoolExecutor(max_workers=3) # <4> 23 | results = executor.map(loiter, range(5)) # <5> 24 | display('results:', results) # <6> 25 | display('Waiting for individual results:') 26 | for i, result in enumerate(results): # <7> 27 | display(f'result {i}: {result}') 28 | 29 | if __name__ == '__main__': 30 | main() 31 | # end::EXECUTOR_MAP[] 32 | -------------------------------------------------------------------------------- /20-executors/getflags/.gitignore: -------------------------------------------------------------------------------- 1 | flags/ 2 | downloaded/ 3 | -------------------------------------------------------------------------------- /20-executors/getflags/country_codes.txt: -------------------------------------------------------------------------------- 1 | AD AE AF AG AL AM AO AR AT AU AZ BA BB BD BE BF BG BH BI BJ BN BO BR BS BT 2 | BW BY BZ CA CD CF CG CH CI CL CM CN CO CR CU CV CY CZ DE DJ DK DM DZ EC EE 3 | EG ER ES ET FI FJ FM FR GA GB GD GE GH GM GN GQ GR GT GW GY HN HR HT HU ID 4 | IE IL IN IQ IR IS IT JM JO JP KE KG KH KI KM KN KP KR KW KZ LA LB LC LI LK 5 | LR LS LT LU LV LY MA MC MD ME MG MH MK ML MM MN MR MT MU MV MW MX MY MZ NA 6 | NE NG NI NL NO NP NR NZ OM PA PE PG PH PK PL PT PW PY QA RO RS RU RW SA SB 7 | SC SD SE SG SI SK SL SM SN SO SR SS ST SV SY SZ TD TG TH TJ TL TM TN TO TR 8 | TT TV TW TZ UA UG US UY UZ VA VC VE VN VU WS YE ZA ZM ZW 9 | -------------------------------------------------------------------------------- /20-executors/getflags/flags.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluentpython/example-code-2e/cf3161ca006b106bfc0ba698e9135e9cdfb51e55/20-executors/getflags/flags.zip -------------------------------------------------------------------------------- /20-executors/getflags/flags_threadpool.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | """Download flags of top 20 countries by population 4 | 5 | ThreadPoolExecutor version 6 | 7 | Sample run:: 8 | 9 | $ python3 flags_threadpool.py 10 | DE FR BD CN EG RU IN TR VN ID JP BR NG MX PK ET PH CD US IR 11 | 20 downloads in 0.35s 12 | 13 | """ 14 | 15 | # tag::FLAGS_THREADPOOL[] 16 | from concurrent import futures 17 | 18 | from flags import save_flag, get_flag, main # <1> 19 | 20 | def download_one(cc: str): # <2> 21 | image = get_flag(cc) 22 | save_flag(image, f'{cc}.gif') 23 | print(cc, end=' ', flush=True) 24 | return cc 25 | 26 | def download_many(cc_list: list[str]) -> int: 27 | with futures.ThreadPoolExecutor() as executor: # <3> 28 | res = executor.map(download_one, sorted(cc_list)) # <4> 29 | 30 | return len(list(res)) # <5> 31 | 32 | if __name__ == '__main__': 33 | main(download_many) # <6> 34 | # end::FLAGS_THREADPOOL[] 35 | -------------------------------------------------------------------------------- /20-executors/getflags/flags_threadpool_futures.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | """Download flags of top 20 countries by population 4 | 5 | ThreadPoolExecutor example with ``as_completed``. 6 | """ 7 | from concurrent import futures 8 | 9 | from flags import main 10 | from flags_threadpool import download_one 11 | 12 | 13 | # tag::FLAGS_THREADPOOL_AS_COMPLETED[] 14 | def download_many(cc_list: list[str]) -> int: 15 | cc_list = cc_list[:5] # <1> 16 | with futures.ThreadPoolExecutor(max_workers=3) as executor: # <2> 17 | to_do: list[futures.Future] = [] 18 | for cc in sorted(cc_list): # <3> 19 | future = executor.submit(download_one, cc) # <4> 20 | to_do.append(future) # <5> 21 | print(f'Scheduled for {cc}: {future}') # <6> 22 | 23 | for count, future in enumerate(futures.as_completed(to_do), 1): # <7> 24 | res: str = future.result() # <8> 25 | print(f'{future} result: {res!r}') # <9> 26 | 27 | return count 28 | # end::FLAGS_THREADPOOL_AS_COMPLETED[] 29 | 30 | if __name__ == '__main__': 31 | main(download_many) 32 | -------------------------------------------------------------------------------- /20-executors/getflags/httpx-error-tree/drawtree.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from tree import tree 4 | 5 | 6 | SP = '\N{SPACE}' 7 | HLIN = '\N{BOX DRAWINGS LIGHT HORIZONTAL}' # ─ 8 | ELBOW = f'\N{BOX DRAWINGS LIGHT UP AND RIGHT}{HLIN*2}{SP}' # └── 9 | TEE = f'\N{BOX DRAWINGS LIGHT VERTICAL AND RIGHT}{HLIN*2}{SP}' # ├── 10 | PIPE = f'\N{BOX DRAWINGS LIGHT VERTICAL}{SP*3}' # │ 11 | 12 | 13 | def cls_name(cls): 14 | module = 'builtins.' if cls.__module__ == 'builtins' else '' 15 | return module + cls.__name__ 16 | 17 | def render_lines(tree_iter): 18 | cls, _, _ = next(tree_iter) 19 | yield cls_name(cls) 20 | prefix = '' 21 | 22 | for cls, level, last in tree_iter: 23 | prefix = prefix[:4 * (level-1)] 24 | prefix = prefix.replace(TEE, PIPE).replace(ELBOW, SP*4) 25 | prefix += ELBOW if last else TEE 26 | yield prefix + cls_name(cls) 27 | 28 | 29 | def draw(cls): 30 | for line in render_lines(tree(cls)): 31 | print(line) 32 | 33 | 34 | if __name__ == '__main__': 35 | draw(Exception) 36 | -------------------------------------------------------------------------------- /20-executors/getflags/httpx-error-tree/tree.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import httpx # make httpx classes available to .__subclasses__() 4 | 5 | 6 | def tree(cls, level=0, last_sibling=True): 7 | yield cls, level, last_sibling 8 | 9 | # get RuntimeError and exceptions defined in httpx 10 | subclasses = [sub for sub in cls.__subclasses__() 11 | if sub is RuntimeError or sub.__module__ == 'httpx'] 12 | if subclasses: 13 | last = subclasses[-1] 14 | for sub in subclasses: 15 | yield from tree(sub, level+1, sub is last) 16 | 17 | 18 | def display(cls): 19 | for cls, level, _ in tree(cls): 20 | indent = ' ' * 4 * level 21 | module = 'builtins.' if cls.__module__ == 'builtins' else '' 22 | print(f'{indent}{module}{cls.__name__}') 23 | 24 | 25 | if __name__ == '__main__': 26 | display(Exception) 27 | -------------------------------------------------------------------------------- /20-executors/getflags/requirements.txt: -------------------------------------------------------------------------------- 1 | anyio==3.3.2 2 | certifi==2024.7.4 3 | charset-normalizer==2.0.6 4 | h11==0.16.0 5 | httpcore==0.13.7 6 | httpx==1.0.0b0 7 | idna==3.7 8 | rfc3986==1.5.0 9 | sniffio==1.2.0 10 | tqdm==4.66.3 11 | -------------------------------------------------------------------------------- /20-executors/primes/primes.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import math 4 | 5 | 6 | PRIME_FIXTURE = [ 7 | (2, True), 8 | (142702110479723, True), 9 | (299593572317531, True), 10 | (3333333333333301, True), 11 | (3333333333333333, False), 12 | (3333335652092209, False), 13 | (4444444444444423, True), 14 | (4444444444444444, False), 15 | (4444444488888889, False), 16 | (5555553133149889, False), 17 | (5555555555555503, True), 18 | (5555555555555555, False), 19 | (6666666666666666, False), 20 | (6666666666666719, True), 21 | (6666667141414921, False), 22 | (7777777536340681, False), 23 | (7777777777777753, True), 24 | (7777777777777777, False), 25 | (9999999999999917, True), 26 | (9999999999999999, False), 27 | ] 28 | 29 | NUMBERS = [n for n, _ in PRIME_FIXTURE] 30 | 31 | # tag::IS_PRIME[] 32 | def is_prime(n: int) -> bool: 33 | if n < 2: 34 | return False 35 | if n == 2: 36 | return True 37 | if n % 2 == 0: 38 | return False 39 | 40 | root = math.isqrt(n) 41 | for i in range(3, root + 1, 2): 42 | if n % i == 0: 43 | return False 44 | return True 45 | # end::IS_PRIME[] 46 | 47 | if __name__ == '__main__': 48 | 49 | for n, prime in PRIME_FIXTURE: 50 | prime_res = is_prime(n) 51 | assert prime_res == prime 52 | print(n, prime) 53 | -------------------------------------------------------------------------------- /21-async/README.rst: -------------------------------------------------------------------------------- 1 | Sample code for Chapter 22 - "Asynchronous programming" 2 | 3 | From the book "Fluent Python, Second Edition" by Luciano Ramalho (O'Reilly, 2021) 4 | https://learning.oreilly.com/library/view/fluent-python-2nd/9781492056348/ 5 | -------------------------------------------------------------------------------- /21-async/domains/README.rst: -------------------------------------------------------------------------------- 1 | domainlib demonstration 2 | ======================= 3 | 4 | Run Python's async console (requires Python ≥ 3.8):: 5 | 6 | $ python3 -m asyncio 7 | 8 | I'll see ``asyncio`` imported automatically:: 9 | 10 | >>> import asyncio 11 | 12 | Now you can experiment with ``domainlib``. 13 | 14 | At the `>>>` prompt, type these commands:: 15 | 16 | >>> from domainlib import * 17 | >>> await probe('python.org') 18 | 19 | Note the result. 20 | 21 | Next:: 22 | 23 | >>> names = 'python.org rust-lang.org golang.org n05uch1an9.org'.split() 24 | >>> async for result in multi_probe(names): 25 | ... print(*result, sep='\t') 26 | 27 | Note that if you run the last two lines again, 28 | the results are likely to appear in a different order. 29 | -------------------------------------------------------------------------------- /21-async/domains/asyncio/blogdom.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import asyncio 3 | import socket 4 | from keyword import kwlist 5 | 6 | MAX_KEYWORD_LEN = 4 # <1> 7 | 8 | 9 | async def probe(domain: str) -> tuple[str, bool]: # <2> 10 | loop = asyncio.get_running_loop() # <3> 11 | try: 12 | await loop.getaddrinfo(domain, None) # <4> 13 | except socket.gaierror: 14 | return (domain, False) 15 | return (domain, True) 16 | 17 | 18 | async def main() -> None: # <5> 19 | names = (kw for kw in kwlist if len(kw) <= MAX_KEYWORD_LEN) # <6> 20 | domains = (f'{name}.dev'.lower() for name in names) # <7> 21 | coros = [probe(domain) for domain in domains] # <8> 22 | for coro in asyncio.as_completed(coros): # <9> 23 | domain, found = await coro # <10> 24 | mark = '+' if found else ' ' 25 | print(f'{mark} {domain}') 26 | 27 | 28 | if __name__ == '__main__': 29 | asyncio.run(main()) # <11> 30 | -------------------------------------------------------------------------------- /21-async/domains/asyncio/domaincheck.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import asyncio 3 | import sys 4 | from keyword import kwlist 5 | 6 | from domainlib import multi_probe 7 | 8 | 9 | async def main(tld: str) -> None: 10 | tld = tld.strip('.') 11 | names = (kw for kw in kwlist if len(kw) <= 4) # <1> 12 | domains = (f'{name}.{tld}'.lower() for name in names) # <2> 13 | print('FOUND\t\tNOT FOUND') # <3> 14 | print('=====\t\t=========') 15 | async for domain, found in multi_probe(domains): # <4> 16 | indent = '' if found else '\t\t' # <5> 17 | print(f'{indent}{domain}') 18 | 19 | 20 | if __name__ == '__main__': 21 | if len(sys.argv) == 2: 22 | asyncio.run(main(sys.argv[1])) # <6> 23 | else: 24 | print('Please provide a TLD.', f'Example: {sys.argv[0]} COM.BR') 25 | -------------------------------------------------------------------------------- /21-async/domains/asyncio/domainlib.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import socket 3 | from collections.abc import Iterable, AsyncIterator 4 | from typing import NamedTuple, Optional 5 | 6 | 7 | class Result(NamedTuple): # <1> 8 | domain: str 9 | found: bool 10 | 11 | 12 | OptionalLoop = Optional[asyncio.AbstractEventLoop] # <2> 13 | 14 | 15 | async def probe(domain: str, loop: OptionalLoop = None) -> Result: # <3> 16 | if loop is None: 17 | loop = asyncio.get_running_loop() 18 | try: 19 | await loop.getaddrinfo(domain, None) 20 | except socket.gaierror: 21 | return Result(domain, False) 22 | return Result(domain, True) 23 | 24 | 25 | async def multi_probe(domains: Iterable[str]) -> AsyncIterator[Result]: # <4> 26 | loop = asyncio.get_running_loop() 27 | coros = [probe(domain, loop) for domain in domains] # <5> 28 | for coro in asyncio.as_completed(coros): # <6> 29 | result = await coro # <7> 30 | yield result # <8> 31 | -------------------------------------------------------------------------------- /21-async/domains/curio/blogdom.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | from curio import run, TaskGroup 3 | import curio.socket as socket 4 | from keyword import kwlist 5 | 6 | MAX_KEYWORD_LEN = 4 7 | 8 | 9 | async def probe(domain: str) -> tuple[str, bool]: # <1> 10 | try: 11 | await socket.getaddrinfo(domain, None) # <2> 12 | except socket.gaierror: 13 | return (domain, False) 14 | return (domain, True) 15 | 16 | async def main() -> None: 17 | names = (kw for kw in kwlist if len(kw) <= MAX_KEYWORD_LEN) 18 | domains = (f'{name}.dev'.lower() for name in names) 19 | async with TaskGroup() as group: # <3> 20 | for domain in domains: 21 | await group.spawn(probe, domain) # <4> 22 | async for task in group: # <5> 23 | domain, found = task.result 24 | mark = '+' if found else ' ' 25 | print(f'{mark} {domain}') 26 | 27 | if __name__ == '__main__': 28 | run(main()) # <6> 29 | -------------------------------------------------------------------------------- /21-async/domains/curio/domaincheck.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import curio 3 | import sys 4 | from keyword import kwlist 5 | 6 | from domainlib import multi_probe 7 | 8 | 9 | async def main(tld: str) -> None: 10 | tld = tld.strip('.') 11 | names = (kw for kw in kwlist if len(kw) <= 4) 12 | domains = (f'{name}.{tld}'.lower() for name in names) 13 | print('FOUND\t\tNOT FOUND') 14 | print('=====\t\t=========') 15 | async for domain, found in multi_probe(domains): 16 | indent = '' if found else '\t\t' 17 | print(f'{indent}{domain}') 18 | 19 | 20 | if __name__ == '__main__': 21 | if len(sys.argv) == 2: 22 | curio.run(main(sys.argv[1])) 23 | else: 24 | print('Please provide a TLD.', f'Example: {sys.argv[0]} COM.BR') 25 | -------------------------------------------------------------------------------- /21-async/domains/curio/domainlib.py: -------------------------------------------------------------------------------- 1 | from collections.abc import Iterable, AsyncIterator 2 | from typing import NamedTuple 3 | 4 | from curio import TaskGroup 5 | import curio.socket as socket 6 | 7 | 8 | class Result(NamedTuple): 9 | domain: str 10 | found: bool 11 | 12 | 13 | async def probe(domain: str) -> Result: 14 | try: 15 | await socket.getaddrinfo(domain, None) 16 | except socket.gaierror: 17 | return Result(domain, False) 18 | return Result(domain, True) 19 | 20 | 21 | async def multi_probe(domains: Iterable[str]) -> AsyncIterator[Result]: 22 | async with TaskGroup() as group: 23 | for domain in domains: 24 | await group.spawn(probe, domain) 25 | async for task in group: 26 | yield task.result 27 | -------------------------------------------------------------------------------- /21-async/domains/curio/requirements.txt: -------------------------------------------------------------------------------- 1 | curio==1.5 2 | -------------------------------------------------------------------------------- /21-async/mojifinder/requirements.txt: -------------------------------------------------------------------------------- 1 | click==7.1.2 2 | fastapi==0.65.2 3 | h11==0.16.0 4 | pydantic==1.10.13 5 | starlette==0.40.0 6 | typing-extensions==3.7.4.3 7 | uvicorn==0.13.4 8 | -------------------------------------------------------------------------------- /21-async/mojifinder/web_mojifinder.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | from unicodedata import name 3 | 4 | from fastapi import FastAPI 5 | from fastapi.responses import HTMLResponse 6 | from pydantic import BaseModel 7 | 8 | from charindex import InvertedIndex 9 | 10 | STATIC_PATH = Path(__file__).parent.absolute() / 'static' # <1> 11 | 12 | app = FastAPI( # <2> 13 | title='Mojifinder Web', 14 | description='Search for Unicode characters by name.', 15 | ) 16 | 17 | class CharName(BaseModel): # <3> 18 | char: str 19 | name: str 20 | 21 | def init(app): # <4> 22 | app.state.index = InvertedIndex() 23 | app.state.form = (STATIC_PATH / 'form.html').read_text() 24 | 25 | init(app) # <5> 26 | 27 | @app.get('/search', response_model=list[CharName]) # <6> 28 | async def search(q: str): # <7> 29 | chars = sorted(app.state.index.search(q)) 30 | return ({'char': c, 'name': name(c)} for c in chars) # <8> 31 | 32 | @app.get('/', response_class=HTMLResponse, include_in_schema=False) 33 | def form(): # <9> 34 | return app.state.form 35 | 36 | # no main funcion # <10> -------------------------------------------------------------------------------- /21-async/mojifinder/web_mojifinder_bottle.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import json 4 | import unicodedata 5 | 6 | from bottle import route, request, run, static_file 7 | 8 | from charindex import InvertedIndex 9 | 10 | index = {} 11 | 12 | @route('/') 13 | def form(): 14 | return static_file('form.html', root='static/') 15 | 16 | 17 | @route('/search') 18 | def search(): 19 | query = request.query['q'] 20 | chars = index.search(query) 21 | results = [] 22 | for char in chars: 23 | name = unicodedata.name(char) 24 | results.append({'char': char, 'name': name}) 25 | return json.dumps(results).encode('UTF-8') 26 | 27 | 28 | def main(port): 29 | global index 30 | index = InvertedIndex() 31 | run(host='localhost', port=port, debug=True) 32 | 33 | 34 | if __name__ == '__main__': 35 | main(8000) 36 | -------------------------------------------------------------------------------- /22-dyn-attr-prop/README.rst: -------------------------------------------------------------------------------- 1 | Sample code for Chapter 19 - "Dynamic attributes and properties" 2 | 3 | From the book "Fluent Python" by Luciano Ramalho (O'Reilly, 2015) 4 | http://shop.oreilly.com/product/0636920032519.do 5 | -------------------------------------------------------------------------------- /22-dyn-attr-prop/bulkfood/bulkfood_v1.py: -------------------------------------------------------------------------------- 1 | """ 2 | A line item for a bulk food order has description, weight and price fields. 3 | A ``subtotal`` method gives the total price for that line item:: 4 | 5 | >>> raisins = LineItem('Golden raisins', 10, 6.95) 6 | >>> raisins.weight, raisins.description, raisins.price 7 | (10, 'Golden raisins', 6.95) 8 | >>> raisins.subtotal() 9 | 69.5 10 | 11 | But, without validation, these public attributes can cause trouble:: 12 | 13 | # tag::LINEITEM_PROBLEM_V1[] 14 | 15 | >>> raisins = LineItem('Golden raisins', 10, 6.95) 16 | >>> raisins.subtotal() 17 | 69.5 18 | >>> raisins.weight = -20 # garbage in... 19 | >>> raisins.subtotal() # garbage out... 20 | -139.0 21 | 22 | # end::LINEITEM_PROBLEM_V1[] 23 | 24 | """ 25 | 26 | 27 | # tag::LINEITEM_V1[] 28 | class LineItem: 29 | 30 | def __init__(self, description, weight, price): 31 | self.description = description 32 | self.weight = weight 33 | self.price = price 34 | 35 | def subtotal(self): 36 | return self.weight * self.price 37 | # end::LINEITEM_V1[] 38 | -------------------------------------------------------------------------------- /22-dyn-attr-prop/doc_property.py: -------------------------------------------------------------------------------- 1 | """ 2 | Example of property documentation 3 | 4 | >>> f = Foo() 5 | >>> f.bar = 77 6 | >>> f.bar 7 | 77 8 | >>> Foo.bar.__doc__ 9 | 'The bar attribute' 10 | """ 11 | 12 | # tag::DOC_PROPERTY[] 13 | class Foo: 14 | 15 | @property 16 | def bar(self): 17 | """The bar attribute""" 18 | return self.__dict__['bar'] 19 | 20 | @bar.setter 21 | def bar(self, value): 22 | self.__dict__['bar'] = value 23 | # end::DOC_PROPERTY[] 24 | -------------------------------------------------------------------------------- /22-dyn-attr-prop/oscon/osconfeed-sample.json: -------------------------------------------------------------------------------- 1 | { "Schedule": 2 | { "conferences": [{"serial": 115 }], 3 | "events": [ 4 | { "serial": 34505, 5 | "name": "Why Schools Don´t Use Open Source to Teach Programming", 6 | "event_type": "40-minute conference session", 7 | "time_start": "2014-07-23 11:30:00", 8 | "time_stop": "2014-07-23 12:10:00", 9 | "venue_serial": 1462, 10 | "description": "Aside from the fact that high school programming...", 11 | "website_url": "http://oscon.com/oscon2014/public/schedule/detail/34505", 12 | "speakers": [157509], 13 | "categories": ["Education"] } 14 | ], 15 | "speakers": [ 16 | { "serial": 157509, 17 | "name": "Robert Lefkowitz", 18 | "photo": null, 19 | "url": "http://sharewave.com/", 20 | "position": "CTO", 21 | "affiliation": "Sharewave", 22 | "twitter": "sharewaveteam", 23 | "bio": "Robert ´r0ml´ Lefkowitz is the CTO at Sharewave, a startup..." } 24 | ], 25 | "venues": [ 26 | { "serial": 1462, 27 | "name": "F151", 28 | "category": "Conference Venues" } 29 | ] 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /22-dyn-attr-prop/oscon/osconfeed_explore.rst: -------------------------------------------------------------------------------- 1 | >>> import json 2 | >>> with open('data/osconfeed.json') as fp: 3 | ... feed = json.load(fp) # <1> 4 | >>> sorted(feed['Schedule'].keys()) # <2> 5 | ['conferences', 'events', 'speakers', 'venues'] 6 | >>> for key, value in sorted(feed['Schedule'].items()): 7 | ... print(f'{len(value):3} {key}') # <3> 8 | ... 9 | 1 conferences 10 | 484 events 11 | 357 speakers 12 | 53 venues 13 | >>> feed['Schedule']['speakers'][-1]['name'] # <4> 14 | 'Carina C. Zona' 15 | >>> feed['Schedule']['speakers'][-1]['serial'] # <5> 16 | 141590 17 | >>> feed['Schedule']['events'][40]['name'] 18 | 'There *Will* Be Bugs' 19 | >>> feed['Schedule']['events'][40]['speakers'] # <6> 20 | [3471, 5199] 21 | -------------------------------------------------------------------------------- /22-dyn-attr-prop/oscon/runtests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | pytest --doctest-modules $2 $1 test_$1 -------------------------------------------------------------------------------- /22-dyn-attr-prop/oscon/schedule_v1.py: -------------------------------------------------------------------------------- 1 | """ 2 | schedule_v1.py: traversing OSCON schedule data 3 | 4 | # tag::SCHEDULE1_DEMO[] 5 | >>> records = load(JSON_PATH) # <1> 6 | >>> speaker = records['speaker.3471'] # <2> 7 | >>> speaker # <3> 8 | 9 | >>> speaker.name, speaker.twitter # <4> 10 | ('Anna Martelli Ravenscroft', 'annaraven') 11 | 12 | # end::SCHEDULE1_DEMO[] 13 | 14 | """ 15 | 16 | # tag::SCHEDULE1[] 17 | import json 18 | 19 | JSON_PATH = 'data/osconfeed.json' 20 | 21 | class Record: 22 | def __init__(self, **kwargs): 23 | self.__dict__.update(kwargs) # <1> 24 | 25 | def __repr__(self): 26 | return f'<{self.__class__.__name__} serial={self.serial!r}>' # <2> 27 | 28 | def load(path=JSON_PATH): 29 | records = {} # <3> 30 | with open(path) as fp: 31 | raw_data = json.load(fp) # <4> 32 | for collection, raw_records in raw_data['Schedule'].items(): # <5> 33 | record_type = collection[:-1] # <6> 34 | for raw_record in raw_records: 35 | key = f'{record_type}.{raw_record["serial"]}' # <7> 36 | records[key] = Record(**raw_record) # <8> 37 | return records 38 | # end::SCHEDULE1[] 39 | -------------------------------------------------------------------------------- /22-dyn-attr-prop/oscon/test_schedule_v1.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | import schedule_v1 as schedule 4 | 5 | 6 | @pytest.fixture 7 | def records(): 8 | yield schedule.load(schedule.JSON_PATH) 9 | 10 | 11 | def test_load(records): 12 | assert len(records) == 895 13 | 14 | 15 | def test_record_attr_access(): 16 | rec = schedule.Record(spam=99, eggs=12) 17 | assert rec.spam == 99 18 | assert rec.eggs == 12 19 | 20 | 21 | def test_venue_record(records): 22 | venue = records['venue.1469'] 23 | assert venue.serial == 1469 24 | assert venue.name == 'Exhibit Hall C' 25 | -------------------------------------------------------------------------------- /22-dyn-attr-prop/pseudo_construction.py: -------------------------------------------------------------------------------- 1 | # pseudocode for object construction 2 | def make(the_class, some_arg): 3 | new_object = the_class.__new__(some_arg) 4 | if isinstance(new_object, the_class): 5 | the_class.__init__(new_object, some_arg) 6 | return new_object 7 | 8 | # the following statements are roughly equivalent 9 | x = Foo('bar') 10 | x = make(Foo, 'bar') 11 | -------------------------------------------------------------------------------- /23-descriptor/README.rst: -------------------------------------------------------------------------------- 1 | Sample code for Chapter 20 - "Attribute descriptors" 2 | 3 | From the book "Fluent Python" by Luciano Ramalho (O'Reilly, 2015) 4 | http://shop.oreilly.com/product/0636920032519.do 5 | -------------------------------------------------------------------------------- /23-descriptor/bulkfood/model_v4c.py: -------------------------------------------------------------------------------- 1 | # BEGIN MODEL_V4 2 | class Quantity: 3 | 4 | def __set_name__(self, owner, name): # <1> 5 | self.storage_name = name # <2> 6 | 7 | def __set__(self, instance, value): # <3> 8 | if value > 0: 9 | instance.__dict__[self.storage_name] = value 10 | else: 11 | msg = f'{self.storage_name} must be > 0' 12 | raise ValueError(msg) 13 | # END MODEL_V4 14 | -------------------------------------------------------------------------------- /23-descriptor/bulkfood/model_v5.py: -------------------------------------------------------------------------------- 1 | # tag::MODEL_V5_VALIDATED_ABC[] 2 | import abc 3 | 4 | class Validated(abc.ABC): 5 | 6 | def __set_name__(self, owner, name): 7 | self.storage_name = name 8 | 9 | def __set__(self, instance, value): 10 | value = self.validate(self.storage_name, value) # <1> 11 | instance.__dict__[self.storage_name] = value # <2> 12 | 13 | @abc.abstractmethod 14 | def validate(self, name, value): # <3> 15 | """return validated value or raise ValueError""" 16 | # end::MODEL_V5_VALIDATED_ABC[] 17 | 18 | # tag::MODEL_V5_VALIDATED_SUB[] 19 | class Quantity(Validated): 20 | """a number greater than zero""" 21 | 22 | def validate(self, name, value): # <1> 23 | if value <= 0: 24 | raise ValueError(f'{name} must be > 0') 25 | return value 26 | 27 | 28 | class NonBlank(Validated): 29 | """a string with at least one non-space character""" 30 | 31 | def validate(self, name, value): 32 | value = value.strip() 33 | if not value: # <2> 34 | raise ValueError(f'{name} cannot be blank') 35 | return value # <3> 36 | # end::MODEL_V5_VALIDATED_SUB[] 37 | -------------------------------------------------------------------------------- /23-descriptor/method_is_descriptor.py: -------------------------------------------------------------------------------- 1 | """ 2 | # tag::FUNC_DESCRIPTOR_DEMO[] 3 | 4 | >>> word = Text('forward') 5 | >>> word # <1> 6 | Text('forward') 7 | >>> word.reverse() # <2> 8 | Text('drawrof') 9 | >>> Text.reverse(Text('backward')) # <3> 10 | Text('drawkcab') 11 | >>> type(Text.reverse), type(word.reverse) # <4> 12 | (, ) 13 | >>> list(map(Text.reverse, ['repaid', (10, 20, 30), Text('stressed')])) # <5> 14 | ['diaper', (30, 20, 10), Text('desserts')] 15 | >>> Text.reverse.__get__(word) # <6> 16 | 17 | >>> Text.reverse.__get__(None, Text) # <7> 18 | 19 | >>> word.reverse # <8> 20 | 21 | >>> word.reverse.__self__ # <9> 22 | Text('forward') 23 | >>> word.reverse.__func__ is Text.reverse # <10> 24 | True 25 | 26 | # end::FUNC_DESCRIPTOR_DEMO[] 27 | """ 28 | 29 | # tag::FUNC_DESCRIPTOR_EX[] 30 | import collections 31 | 32 | 33 | class Text(collections.UserString): 34 | 35 | def __repr__(self): 36 | return 'Text({!r})'.format(self.data) 37 | 38 | def reverse(self): 39 | return self[::-1] 40 | 41 | # end::FUNC_DESCRIPTOR_EX[] 42 | -------------------------------------------------------------------------------- /24-class-metaprog/autoconst/autoconst.py: -------------------------------------------------------------------------------- 1 | # tag::WilyDict[] 2 | class WilyDict(dict): 3 | def __init__(self, *args, **kwargs): 4 | super().__init__(*args, **kwargs) 5 | self.__next_value = 0 6 | 7 | def __missing__(self, key): 8 | if key.startswith('__') and key.endswith('__'): 9 | raise KeyError(key) 10 | self[key] = value = self.__next_value 11 | self.__next_value += 1 12 | return value 13 | # end::WilyDict[] 14 | 15 | # tag::AUTOCONST[] 16 | class AutoConstMeta(type): 17 | def __prepare__(name, bases, **kwargs): 18 | return WilyDict() 19 | 20 | class AutoConst(metaclass=AutoConstMeta): 21 | pass 22 | # end::AUTOCONST[] 23 | -------------------------------------------------------------------------------- /24-class-metaprog/autoconst/autoconst_demo.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | """ 4 | Testing ``WilyDict``:: 5 | 6 | >>> from autoconst import WilyDict 7 | >>> wd = WilyDict() 8 | >>> len(wd) 9 | 0 10 | >>> wd['first'] 11 | 0 12 | >>> wd 13 | {'first': 0} 14 | >>> wd['second'] 15 | 1 16 | >>> wd['third'] 17 | 2 18 | >>> len(wd) 19 | 3 20 | >>> wd 21 | {'first': 0, 'second': 1, 'third': 2} 22 | >>> wd['__magic__'] 23 | Traceback (most recent call last): 24 | ... 25 | KeyError: '__magic__' 26 | 27 | Testing ``AutoConst``:: 28 | 29 | >>> from autoconst import AutoConst 30 | 31 | # tag::AUTOCONST[] 32 | >>> class Flavor(AutoConst): 33 | ... banana 34 | ... coconut 35 | ... vanilla 36 | ... 37 | >>> Flavor.vanilla 38 | 2 39 | >>> Flavor.banana, Flavor.coconut 40 | (0, 1) 41 | 42 | # end::AUTOCONST[] 43 | 44 | """ 45 | 46 | from autoconst import AutoConst 47 | 48 | 49 | class Flavor(AutoConst): 50 | banana 51 | coconut 52 | vanilla 53 | 54 | 55 | print('Flavor.vanilla ==', Flavor.vanilla) -------------------------------------------------------------------------------- /24-class-metaprog/checked/decorator/checkeddeco_demo.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from checkeddeco import checked 4 | 5 | @checked 6 | class Movie: 7 | title: str 8 | year: int 9 | box_office: float 10 | 11 | 12 | if __name__ == '__main__': 13 | # No static type checker can understand this... 14 | movie = Movie(title='The Godfather', year=1972, box_office=137) # type: ignore 15 | print(movie.title) 16 | print(movie) 17 | try: 18 | # remove the "type: ignore" comment to see Mypy correctly spot the error 19 | movie.year = 'MCMLXXII' # type: ignore 20 | except TypeError as e: 21 | print(e) 22 | try: 23 | # Again, no static type checker can understand this... 24 | blockbuster = Movie(title='Avatar', year=2009, box_office='billions') # type: ignore 25 | except TypeError as e: 26 | print(e) 27 | -------------------------------------------------------------------------------- /24-class-metaprog/checked/decorator/checkeddeco_test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | from checkeddeco import checked 5 | 6 | 7 | def test_field_descriptor_validation_type_error(): 8 | @checked 9 | class Cat: 10 | name: str 11 | weight: float 12 | 13 | with pytest.raises(TypeError) as e: 14 | felix = Cat(name='Felix', weight=None) 15 | 16 | assert str(e.value) == 'None is not compatible with weight:float' 17 | 18 | 19 | def test_field_descriptor_validation_value_error(): 20 | @checked 21 | class Cat: 22 | name: str 23 | weight: float 24 | 25 | with pytest.raises(TypeError) as e: 26 | felix = Cat(name='Felix', weight='half stone') 27 | 28 | assert str(e.value) == "'half stone' is not compatible with weight:float" 29 | 30 | 31 | def test_constructor_attribute_error(): 32 | @checked 33 | class Cat: 34 | name: str 35 | weight: float 36 | 37 | with pytest.raises(AttributeError) as e: 38 | felix = Cat(name='Felix', weight=3.2, age=7) 39 | 40 | assert str(e.value) == "'Cat' has no attribute 'age'" 41 | 42 | 43 | def test_field_invalid_constructor(): 44 | with pytest.raises(TypeError) as e: 45 | @checked 46 | class Cat: 47 | name: str 48 | weight: None 49 | 50 | assert str(e.value) == "'weight' type hint must be callable" -------------------------------------------------------------------------------- /24-class-metaprog/checked/initsub/checked_demo.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from checkedlib import Checked 4 | 5 | class Movie(Checked): 6 | title: str 7 | year: int 8 | box_office: float 9 | 10 | 11 | if __name__ == '__main__': 12 | movie = Movie(title='The Godfather', year=1972, box_office=137) 13 | print(movie.title) 14 | print(movie) 15 | try: 16 | # remove the "type: ignore" comment to see Mypy error 17 | movie.year = 'MCMLXXII' # type: ignore 18 | except TypeError as e: 19 | print(e) 20 | try: 21 | blockbuster = Movie(title='Avatar', year=2009, box_office='billions') 22 | except TypeError as e: 23 | print(e) 24 | -------------------------------------------------------------------------------- /24-class-metaprog/checked/metaclass/checked_demo.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # tag::MOVIE_DEMO[] 4 | from checkedlib import Checked 5 | 6 | class Movie(Checked): 7 | title: str 8 | year: int 9 | box_office: float 10 | 11 | if __name__ == '__main__': 12 | movie = Movie(title='The Godfather', year=1972, box_office=137) 13 | print(movie) 14 | print(movie.title) 15 | # end::MOVIE_DEMO[] 16 | 17 | try: 18 | # remove the "type: ignore" comment to see Mypy error 19 | movie.year = 'MCMLXXII' # type: ignore 20 | except TypeError as e: 21 | print(e) 22 | try: 23 | blockbuster = Movie(title='Avatar', year=2009, box_office='billions') 24 | except TypeError as e: 25 | print(e) 26 | -------------------------------------------------------------------------------- /24-class-metaprog/evaltime/evaldemo.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from builderlib import Builder, deco, Descriptor 4 | 5 | print('# evaldemo module start') 6 | 7 | @deco # <1> 8 | class Klass(Builder): # <2> 9 | print('# Klass body') 10 | 11 | attr = Descriptor() # <3> 12 | 13 | def __init__(self): 14 | super().__init__() 15 | print(f'# Klass.__init__({self!r})') 16 | 17 | def __repr__(self): 18 | return '' 19 | 20 | 21 | def main(): # <4> 22 | obj = Klass() 23 | obj.method_a() 24 | obj.method_b() 25 | obj.attr = 999 26 | 27 | if __name__ == '__main__': 28 | main() 29 | 30 | print('# evaldemo module end') 31 | -------------------------------------------------------------------------------- /24-class-metaprog/evaltime/evaldemo_meta.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from builderlib import Builder, deco, Descriptor 4 | from metalib import MetaKlass # <1> 5 | 6 | print('# evaldemo_meta module start') 7 | 8 | @deco 9 | class Klass(Builder, metaclass=MetaKlass): # <2> 10 | print('# Klass body') 11 | 12 | attr = Descriptor() 13 | 14 | def __init__(self): 15 | super().__init__() 16 | print(f'# Klass.__init__({self!r})') 17 | 18 | def __repr__(self): 19 | return '' 20 | 21 | 22 | def main(): 23 | obj = Klass() 24 | obj.method_a() 25 | obj.method_b() 26 | obj.method_c() # <3> 27 | obj.attr = 999 28 | 29 | 30 | if __name__ == '__main__': 31 | main() 32 | 33 | print('# evaldemo_meta module end') 34 | -------------------------------------------------------------------------------- /24-class-metaprog/metabunch/README.md: -------------------------------------------------------------------------------- 1 | # Examples from Python in a Nutshell, 3rd edition 2 | 3 | The metaclass `MetaBunch` example in `original/bunch.py` is an exact copy of the 4 | last example in the _How a Metaclass Creates a Class_ section of 5 | _Chapter 4: Object Oriented Python_ from 6 | [_Python in a Nutshell, 3rd edition_](https://learning.oreilly.com/library/view/python-in-a/9781491913833) 7 | by Alex Martelli, Anna Ravenscroft, and Steve Holden. 8 | 9 | The version in `pre3.6/bunch.py` is slightly simplified by taking advantage 10 | of Python 3 `super()` and removing comments and docstrings, 11 | to make it easier to compare to the `from3.6` version. 12 | 13 | The version in `from3.6/bunch.py` is further simplified by taking advantage 14 | of the order-preserving `dict` that appeared in Python 3.6, 15 | as well as other simplifications, 16 | such as leveraging closures in `__init__` and `__repr__` 17 | to avoid adding a `__defaults__` mapping to the class. 18 | 19 | The external behavior of all three versions is the same, and 20 | the test files `bunch_test.py` are identical in the three directories. 21 | -------------------------------------------------------------------------------- /24-class-metaprog/metabunch/nutshell3e/bunch_test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from bunch import Bunch 4 | 5 | class Point(Bunch): 6 | """ A point has x and y coordinates, defaulting to 0.0, 7 | and a color, defaulting to 'gray'—and nothing more, 8 | except what Python and the metaclass conspire to add, 9 | such as __init__ and __repr__ 10 | """ 11 | x = 0.0 12 | y = 0.0 13 | color = 'gray' 14 | 15 | 16 | def test_init_defaults(): 17 | p = Point() 18 | assert repr(p) == 'Point()' 19 | 20 | 21 | def test_init(): 22 | p = Point(x=1.2, y=3.4, color='red') 23 | assert repr(p) == "Point(x=1.2, y=3.4, color='red')" 24 | 25 | 26 | def test_init_wrong_argument(): 27 | with pytest.raises(AttributeError) as exc: 28 | p = Point(x=1.2, y=3.4, flavor='coffee') 29 | assert "no attribute 'flavor'" in str(exc.value) 30 | 31 | 32 | def test_slots(): 33 | p = Point() 34 | with pytest.raises(AttributeError) as exc: 35 | p.z = 5.6 36 | assert "no attribute 'z'" in str(exc.value) 37 | 38 | 39 | -------------------------------------------------------------------------------- /24-class-metaprog/metabunch/original/bunch_test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from bunch import MetaBunch 4 | 5 | class Point(MetaBunch): 6 | """ A point has x and y coordinates, defaulting to 0.0, 7 | and a color, defaulting to 'gray'—and nothing more, 8 | except what Python and the metaclass conspire to add, 9 | such as __init__ and __repr__ 10 | """ 11 | x = 0.0 12 | y = 0.0 13 | color = 'gray' 14 | 15 | 16 | def test_init_defaults(): 17 | p = Point() 18 | assert repr(p) == 'Point()' 19 | 20 | 21 | def test_init(): 22 | p = Point(x=1.2, y=3.4, color='red') 23 | assert repr(p) == "Point(x=1.2, y=3.4, color='red')" 24 | 25 | 26 | def test_init_wrong_argument(): 27 | with pytest.raises(AttributeError) as exc: 28 | p = Point(x=1.2, y=3.4, flavor='coffee') 29 | assert "no attribute 'flavor'" in str(exc.value) 30 | 31 | 32 | def test_slots(): 33 | p = Point() 34 | with pytest.raises(AttributeError) as exc: 35 | p.z = 5.6 36 | assert "no attribute 'z'" in str(exc.value) 37 | 38 | 39 | -------------------------------------------------------------------------------- /24-class-metaprog/metabunch/pre3.6/bunch_test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from bunch import Bunch 4 | 5 | class Point(Bunch): 6 | """ A point has x and y coordinates, defaulting to 0.0, 7 | and a color, defaulting to 'gray'—and nothing more, 8 | except what Python and the metaclass conspire to add, 9 | such as __init__ and __repr__ 10 | """ 11 | x = 0.0 12 | y = 0.0 13 | color = 'gray' 14 | 15 | 16 | def test_init_defaults(): 17 | p = Point() 18 | assert repr(p) == 'Point()' 19 | 20 | 21 | def test_init(): 22 | p = Point(x=1.2, y=3.4, color='red') 23 | assert repr(p) == "Point(x=1.2, y=3.4, color='red')" 24 | 25 | 26 | def test_init_wrong_argument(): 27 | with pytest.raises(AttributeError) as exc: 28 | p = Point(x=1.2, y=3.4, flavor='coffee') 29 | assert "no attribute 'flavor'" in str(exc.value) 30 | 31 | 32 | def test_slots(): 33 | p = Point() 34 | with pytest.raises(AttributeError) as exc: 35 | p.z = 5.6 36 | assert "no attribute 'z'" in str(exc.value) 37 | 38 | 39 | -------------------------------------------------------------------------------- /24-class-metaprog/persistent/.gitignore: -------------------------------------------------------------------------------- 1 | *.db 2 | -------------------------------------------------------------------------------- /24-class-metaprog/persistent/persistlib_test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | from persistlib import Persistent 5 | 6 | 7 | def test_field_descriptor_validation_type_error(): 8 | class Cat(Persistent): 9 | name: str 10 | weight: float 11 | 12 | with pytest.raises(TypeError) as e: 13 | felix = Cat(name='Felix', weight=None) 14 | 15 | assert str(e.value) == 'None is not compatible with weight:float.' 16 | 17 | 18 | def test_field_descriptor_validation_value_error(): 19 | class Cat(Persistent): 20 | name: str 21 | weight: float 22 | 23 | with pytest.raises(TypeError) as e: 24 | felix = Cat(name='Felix', weight='half stone') 25 | 26 | assert str(e.value) == "'half stone' is not compatible with weight:float." 27 | 28 | 29 | def test_constructor_attribute_error(): 30 | class Cat(Persistent): 31 | name: str 32 | weight: float 33 | 34 | with pytest.raises(AttributeError) as e: 35 | felix = Cat(name='Felix', weight=3.2, age=7) 36 | 37 | assert str(e.value) == "'Cat' has no attribute 'age'" 38 | -------------------------------------------------------------------------------- /24-class-metaprog/qualname/fakedjango.py: -------------------------------------------------------------------------------- 1 | class models: 2 | class Model: 3 | "nothing to see here" 4 | class IntegerField: 5 | "nothing to see here" 6 | -------------------------------------------------------------------------------- /24-class-metaprog/qualname/models.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from fakedjango import models 4 | 5 | class Ox(models.Model): 6 | horn_length = models.IntegerField() 7 | 8 | class Meta: 9 | ordering = ['horn_length'] 10 | verbose_name_plural = 'oxen' 11 | 12 | print(Ox.Meta.__name__) 13 | print(Ox.Meta.__qualname__) 14 | -------------------------------------------------------------------------------- /24-class-metaprog/sentinel/sentinel.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module provides a ``Sentinel`` class that can be used directly as a 3 | sentinel singleton, or subclassed if a distinct sentinel singleton is needed. 4 | 5 | The ``repr`` of a ``Sentinel`` class is its name:: 6 | 7 | >>> class Missing(Sentinel): pass 8 | >>> Missing 9 | Missing 10 | 11 | If a different ``repr`` is required, 12 | you can define it as a class attribute:: 13 | 14 | >>> class CustomRepr(Sentinel): 15 | ... repr = '' 16 | ... 17 | >>> CustomRepr 18 | 19 | 20 | ``Sentinel`` classes cannot be instantiated:: 21 | 22 | >>> Missing() 23 | Traceback (most recent call last): 24 | ... 25 | TypeError: 'Missing' is a sentinel and cannot be instantiated 26 | 27 | """ 28 | 29 | 30 | class _SentinelMeta(type): 31 | def __repr__(cls): 32 | try: 33 | return cls.repr 34 | except AttributeError: 35 | return cls.__name__ 36 | 37 | 38 | class Sentinel(metaclass=_SentinelMeta): 39 | def __new__(cls): 40 | msg = 'is a sentinel and cannot be instantiated' 41 | raise TypeError(f"'{cls!r}' {msg}") 42 | -------------------------------------------------------------------------------- /24-class-metaprog/sentinel/sentinel_test.py: -------------------------------------------------------------------------------- 1 | import pickle 2 | 3 | import pytest 4 | 5 | from sentinel import Sentinel 6 | 7 | 8 | class PlainSentinel(Sentinel): 9 | pass 10 | 11 | 12 | class SentinelCustomRepr(Sentinel): 13 | repr = '***SentinelRepr***' 14 | 15 | 16 | def test_repr(): 17 | assert repr(PlainSentinel) == 'PlainSentinel' 18 | 19 | 20 | def test_cannot_instantiate(): 21 | with pytest.raises(TypeError) as e: 22 | PlainSentinel() 23 | msg = "'PlainSentinel' is a sentinel and cannot be instantiated" 24 | assert msg in str(e.value) 25 | 26 | 27 | def test_custom_repr(): 28 | assert repr(SentinelCustomRepr) == '***SentinelRepr***' 29 | 30 | 31 | def test_pickle(): 32 | s = pickle.dumps(SentinelCustomRepr) 33 | ps = pickle.loads(s) 34 | assert ps is SentinelCustomRepr 35 | 36 | 37 | def test_sentinel_comes_ready_to_use(): 38 | assert repr(Sentinel) == 'Sentinel' 39 | s = pickle.dumps(Sentinel) 40 | ps = pickle.loads(s) 41 | assert ps is Sentinel 42 | -------------------------------------------------------------------------------- /24-class-metaprog/setattr/example_from_leo.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | class Foo: 4 | @property 5 | def bar(self): 6 | return self._bar 7 | 8 | @bar.setter 9 | def bar(self, value): 10 | self._bar = value 11 | 12 | def __setattr__(self, name, value): 13 | print(f'setting {name!r} to {value!r}') 14 | super().__setattr__(name, value) 15 | 16 | o = Foo() 17 | o.bar = 8 18 | print(o.bar) 19 | print(o._bar) 20 | -------------------------------------------------------------------------------- /24-class-metaprog/slots/slots_timing.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | class Wrong: 4 | 5 | def __init_subclass__(subclass): 6 | subclass.__slots__ = ('x', 'y') 7 | 8 | 9 | class Klass0(Wrong): 10 | pass 11 | 12 | 13 | o = Klass0() 14 | o.z = 3 15 | print('o.z = 3 # did not raise Attribute error because __slots__ was created too late') 16 | 17 | 18 | class Correct1(type): 19 | 20 | def __new__(meta_cls, cls_name, bases, cls_dict): 21 | cls_dict['__slots__'] = ('x', 'y') 22 | return super().__new__( 23 | meta_cls, cls_name, bases, cls_dict) 24 | 25 | 26 | class Klass1(metaclass=Correct1): 27 | pass 28 | 29 | o = Klass1() 30 | try: 31 | o.z = 3 32 | except AttributeError as e: 33 | print('Raised as expected:', e) 34 | 35 | 36 | class Correct2(type): 37 | def __prepare__(name, bases): 38 | return dict(__slots__=('x', 'y')) 39 | 40 | class Klass2(metaclass=Correct2): 41 | pass 42 | 43 | o = Klass2() 44 | try: 45 | o.z = 3 46 | except AttributeError as e: 47 | print('Raised as expected:', e) 48 | 49 | -------------------------------------------------------------------------------- /24-class-metaprog/tinyenums/microenum_demo.py: -------------------------------------------------------------------------------- 1 | """ 2 | Testing ``Flavor``:: 3 | 4 | >>> Flavor.cocoa, Flavor.coconut, Flavor.vanilla 5 | (0, 1, 2) 6 | >>> Flavor[1] 7 | 'coconut' 8 | 9 | """ 10 | 11 | from microenum import MicroEnum 12 | 13 | 14 | class Flavor(MicroEnum): 15 | cocoa 16 | coconut 17 | vanilla 18 | -------------------------------------------------------------------------------- /24-class-metaprog/tinyenums/nanoenum.py: -------------------------------------------------------------------------------- 1 | # This is a simplification of an idea by João S. O. Bueno (@gwidion) 2 | # shared privately with me, with permission to use in Fluent Python 2e. 3 | 4 | """ 5 | Testing ``KeyIsValueDict``:: 6 | 7 | >>> adict = KeyIsValueDict() 8 | >>> len(adict) 9 | 0 10 | >>> adict['first'] 11 | 'first' 12 | >>> adict 13 | {'first': 'first'} 14 | >>> adict['second'] 15 | 'second' 16 | >>> len(adict) 17 | 2 18 | >>> adict 19 | {'first': 'first', 'second': 'second'} 20 | >>> adict['__magic__'] 21 | Traceback (most recent call last): 22 | ... 23 | KeyError: '__magic__' 24 | """ 25 | 26 | 27 | class KeyIsValueDict(dict): 28 | def __missing__(self, key): 29 | if key.startswith('__') and key.endswith('__'): 30 | raise KeyError(key) 31 | self[key] = key 32 | return key 33 | 34 | 35 | class NanoEnumMeta(type): 36 | def __prepare__(name, bases, **kwargs): 37 | return KeyIsValueDict() 38 | 39 | 40 | class NanoEnum(metaclass=NanoEnumMeta): 41 | pass 42 | -------------------------------------------------------------------------------- /24-class-metaprog/tinyenums/nanoenum_demo.py: -------------------------------------------------------------------------------- 1 | """ 2 | Testing ``Flavor``:: 3 | 4 | >>> Flavor.coconut 5 | 'coconut' 6 | >>> Flavor.cocoa, Flavor.vanilla 7 | ('cocoa', 'vanilla') 8 | 9 | """ 10 | 11 | from nanoenum import NanoEnum 12 | 13 | 14 | class Flavor(NanoEnum): 15 | cocoa 16 | coconut 17 | vanilla 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2021 Luciano Ramalho 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /links/README.md: -------------------------------------------------------------------------------- 1 | This file is deployed as `.htaccess` to the FPY.LI domain 2 | to map short URLs in Fluent Python to the original URLs. 3 | 4 | To update it, I use tools in this other repo: 5 | 6 | https://github.com/pythonfluente/pythonfluente2e 7 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | addopts = --doctest-modules 3 | -------------------------------------------------------------------------------- /ruff.toml: -------------------------------------------------------------------------------- 1 | line-length = 100 2 | [format] 3 | # Like Python's repr(), use single quotes for strings. 4 | quote-style = "single" --------------------------------------------------------------------------------