From 6527037ae7319ba370a1ee2d9fe79214d0ed9452 Mon Sep 17 00:00:00 2001 From: Luciano Ramalho Date: Wed, 15 Sep 2021 22:48:08 -0300 Subject: [PATCH] moved lispy from 02 to 18 --- 02-array-seq/lispy/py3.10/examples_test.py | 109 -------- 02-array-seq/lispy/py3.10/meta_test.py | 69 ----- 02-array-seq/lispy/py3.9-no-hints/README.md | 33 --- 02-array-seq/lispy/py3.9-no-hints/lis.py | 142 ---------- 02-array-seq/lispy/py3.9-no-hints/lis_test.py | 166 ----------- 05-data-classes/dataclass/hackerclub.py | 2 +- .../dataclass/hackerclub_annotated.py | 2 +- 17-it-generator/aritprog_v3.py | 6 +- 17-it-generator/coroaverager.py | 43 +++ 17-it-generator/coroaverager2.py | 96 +++++++ 17-it-generator/fibo_gen.py | 7 + 17-it-generator/sentence_gen.py | 2 +- 18-with-match/lisplus/examples_test.py | 109 -------- 18-with-match/lisplus/lis.py | 192 ------------- 18-with-match/lisplus/lis_test.py | 182 ------------ 18-with-match/lisplus/meta_test.py | 64 ----- {02-array-seq => 18-with-match}/lispy/LICENSE | 0 .../lispy/README.md | 0 .../lispy/original/LICENSE | 0 .../lispy/original/README.md | 0 .../lispy/original/lis.py | 0 .../lispy/original/lispy.py | 0 .../lispy/original/lispytest.py | 0 18-with-match/lispy/py3.10/examples_test.py | 259 ++++++++++++++++++ .../lispy/py3.10/lis.py | 182 ++++++------ .../lispy/py3.10/lis_test.py | 0 18-with-match/lispy/py3.10/quicksort.scm | 17 ++ .../lispy/py3.9/README.md | 0 .../lispy/py3.9/lis.py | 18 +- .../lispy/py3.9/lis_test.py | 0 18-with-match/mirror.py | 28 +- 18-with-match/mirror_gen.py | 22 +- 18-with-match/mirror_gen_exc.py | 9 +- 20-futures/getflags/flags2_asyncio.py | 4 +- 20-futures/getflags/flags3_asyncio.py | 4 +- 20-futures/getflags/requirements.txt | 16 +- 20-futures/getflags/slow_server.py | 1 + 22-dyn-attr-prop/oscon/demo_schedule2.py | 25 -- 38 files changed, 590 insertions(+), 1219 deletions(-) delete mode 100644 02-array-seq/lispy/py3.10/examples_test.py delete mode 100644 02-array-seq/lispy/py3.10/meta_test.py delete mode 100644 02-array-seq/lispy/py3.9-no-hints/README.md delete mode 100644 02-array-seq/lispy/py3.9-no-hints/lis.py delete mode 100644 02-array-seq/lispy/py3.9-no-hints/lis_test.py create mode 100644 17-it-generator/coroaverager.py create mode 100644 17-it-generator/coroaverager2.py create mode 100644 17-it-generator/fibo_gen.py delete mode 100644 18-with-match/lisplus/examples_test.py delete mode 100644 18-with-match/lisplus/lis.py delete mode 100644 18-with-match/lisplus/lis_test.py delete mode 100644 18-with-match/lisplus/meta_test.py rename {02-array-seq => 18-with-match}/lispy/LICENSE (100%) rename {02-array-seq => 18-with-match}/lispy/README.md (100%) rename {02-array-seq => 18-with-match}/lispy/original/LICENSE (100%) rename {02-array-seq => 18-with-match}/lispy/original/README.md (100%) rename {02-array-seq => 18-with-match}/lispy/original/lis.py (100%) rename {02-array-seq => 18-with-match}/lispy/original/lispy.py (100%) rename {02-array-seq => 18-with-match}/lispy/original/lispytest.py (100%) create mode 100644 18-with-match/lispy/py3.10/examples_test.py rename {02-array-seq => 18-with-match}/lispy/py3.10/lis.py (70%) mode change 100644 => 100755 rename {02-array-seq => 18-with-match}/lispy/py3.10/lis_test.py (100%) create mode 100644 18-with-match/lispy/py3.10/quicksort.scm rename {02-array-seq => 18-with-match}/lispy/py3.9/README.md (100%) rename {02-array-seq => 18-with-match}/lispy/py3.9/lis.py (91%) rename {02-array-seq => 18-with-match}/lispy/py3.9/lis_test.py (100%) delete mode 100755 22-dyn-attr-prop/oscon/demo_schedule2.py diff --git a/02-array-seq/lispy/py3.10/examples_test.py b/02-array-seq/lispy/py3.10/examples_test.py deleted file mode 100644 index 38510a3..0000000 --- a/02-array-seq/lispy/py3.10/examples_test.py +++ /dev/null @@ -1,109 +0,0 @@ -""" -Doctests for `parse`: - ->>> from lis import parse - -# tag::PARSE_DEMO[] ->>> parse('1.5') # <1> -1.5 ->>> parse('set!') # <2> -'set!' ->>> parse('(gcd 18 44)') # <3> -['gcd', 18, 44] ->>> parse('(- m (* n (// m n)))') # <4> -['-', 'm', ['*', 'n', ['//', 'm', 'n']]] - -# end::PARSE_DEMO[] - -""" - -import math - -from lis import run - - -fact_src = """ -(define (! n) - (if (< n 2) - 1 - (* n (! (- n 1))) - ) -) -(! 42) -""" -def test_factorial(): - got = run(fact_src) - assert got == 1405006117752879898543142606244511569936384000000000 - assert got == math.factorial(42) - - -gcd_src = """ -(define (mod m n) - (- m (* n (// m n)))) -(define (gcd m n) - (if (= n 0) - m - (gcd n (mod m n)))) -(gcd 18 45) -""" -def test_gcd(): - got = run(gcd_src) - assert got == 9 - - -quicksort_src = """ -(define (quicksort lst) - (if (null? lst) - lst - (begin - (define pivot (car lst)) - (define rest (cdr lst)) - (append - (quicksort - (filter (lambda (x) (< x pivot)) rest)) - (list pivot) - (quicksort - (filter (lambda (x) (>= x pivot)) rest))) - ) - ) -) -(quicksort (list 2 1 6 3 4 0 8 9 7 5)) -""" -def test_quicksort(): - got = run(quicksort_src) - assert got == [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] - - -# Example from Structure and Interpretation of Computer Programs -# https://mitpress.mit.edu/sites/default/files/sicp/full-text/sicp/book/node12.html - -newton_src = """ -(define (sqrt x) - (sqrt-iter 1.0 x)) -(define (sqrt-iter guess x) - (if (good-enough? guess x) - guess - (sqrt-iter (improve guess x) x))) -(define (good-enough? guess x) - (< (abs (- (* guess guess) x)) 0.001)) -(define (improve guess x) - (average guess (/ x guess))) -(define (average x y) - (/ (+ x y) 2)) -(sqrt 123454321) -""" -def test_newton(): - got = run(newton_src) - assert math.isclose(got, 11111) - - -closure_src = """ -(define (make-adder increment) - (lambda (x) (+ increment x)) -) -(define inc (make-adder 1)) -(inc 99) -""" -def test_newton(): - got = run(closure_src) - assert got == 100 diff --git a/02-array-seq/lispy/py3.10/meta_test.py b/02-array-seq/lispy/py3.10/meta_test.py deleted file mode 100644 index 3ddc3f3..0000000 --- a/02-array-seq/lispy/py3.10/meta_test.py +++ /dev/null @@ -1,69 +0,0 @@ -""" -Tests for developing a meta-circular interpreter, step-by-step. -""" - - -import operator as op - -from lis import run - -env_scm = """ -(define standard-env (list - (list (quote +) +) - (list (quote -) -) -)) -standard-env -""" - -def test_env_build(): - got = run(env_scm) - assert got == [['+', op.add], ['-', op.sub]] - -scan_scm = """ -(define l (quote (a b c))) -(define (scan what where) - (if (null? where) - () - (if (eq? what (car where)) - what - (scan what (cdr where)))) -) -""" - -def test_scan(): - source = scan_scm + '(scan (quote a) l )' - got = run(source) - assert got == 'a' - - -def test_scan_not_found(): - source = scan_scm + '(scan (quote z) l )' - got = run(source) - assert got == [] - - -lookup_scm = """ -(define env (list - (list (quote +) +) - (list (quote -) -) -)) -(define (lookup what where) - (if (null? where) - () - (if (eq? what (car (car where))) - (car (cdr (car where))) - (lookup what (cdr where)))) -) -""" - -def test_lookup(): - source = lookup_scm + '(lookup (quote +) env)' - got = run(source) - assert got == op.add - - -def test_lookup_not_found(): - source = lookup_scm + '(lookup (quote z) env )' - got = run(source) - assert got == [] - diff --git a/02-array-seq/lispy/py3.9-no-hints/README.md b/02-array-seq/lispy/py3.9-no-hints/README.md deleted file mode 100644 index 2b6f4ea..0000000 --- a/02-array-seq/lispy/py3.9-no-hints/README.md +++ /dev/null @@ -1,33 +0,0 @@ -# Changes from the original - -While adapting Peter Norvig's [lis.py](https://github.com/norvig/pytudes/blob/705c0a335c1811a203e79587d7d41865cf7f41c7/py/lis.py) for -use in _Fluent Python, Second Edition_, I made a few changes for didactic reasons. - -_Luciano Ramalho_ - -## Significant changes - -* Make the `lambda` form accept more than one expression as the body. This is consistent with _Scheme_ syntax, and provides a useful example for the book. To implement this: - * In `Procedure.__call__`: evaluate `self.body` as a list of expressions, instead of a single expression. Return the value of the last expression. - * In `evaluate()`: when processing `lambda`, unpack expression into `(_, parms, *body)`, to accept a list of expressions as the body. -* Remove the `global_env` global `dict`. It is only used as a default value for the `env` parameter in `evaluate()`, but it is unsafe to use mutable data structures as parameter default values. To implement this: - * In `repl()`: create local variable `global_env` and pass it as the `env` paramater of `evaluate()`. - * In `evaluate()`, remove `global_env` default value for `env`. -* Rewrite the custom test script -[lispytest.py](https://github.com/norvig/pytudes/blob/705c0a335c1811a203e79587d7d41865cf7f41c7/py/lispytest.py) as -[lis_test.py](https://github.com/fluentpython/example-code-2e/blob/master/02-array-seq/lispy/py3.9/lis_test.py): -a standard [pytest](https://docs.pytest.org) test suite including new test cases, preserving all Norvig's the test cases for -[lis.py](https://github.com/norvig/pytudes/blob/705c0a335c1811a203e79587d7d41865cf7f41c7/py/lis.py) -but removing the test cases for the features implemented only in -[lispy.py](https://github.com/norvig/pytudes/blob/705c0a335c1811a203e79587d7d41865cf7f41c7/py/lispy.py) - - -## Name changes - -Cosmetic changes to make the code look more familiar to -Python programmers, the audience of _Fluent Python_. - -* Rename `eval()` to `evaluate()`, to avoid confusion with Python's `eval` built-in function. -* Refer to the list class as `list` instead of aliasing as `List`, to avoid confusion with `typing.List` which is often imported as `List`. -* Import `collections.ChainMap` as `ChainMap` instead of `Environment`. - diff --git a/02-array-seq/lispy/py3.9-no-hints/lis.py b/02-array-seq/lispy/py3.9-no-hints/lis.py deleted file mode 100644 index c74c3ae..0000000 --- a/02-array-seq/lispy/py3.9-no-hints/lis.py +++ /dev/null @@ -1,142 +0,0 @@ -################ Lispy: Scheme Interpreter in Python 3.9 - -## (c) Peter Norvig, 2010-18; See http://norvig.com/lispy.html -## Minor edits for Fluent Python, Second Edition (O'Reilly, 2021) -## by Luciano Ramalho, adding type hints and pattern matching. - -################ Imports and Types - -import math -import operator as op -from collections import ChainMap -from typing import Any - -Symbol = str # A Lisp Symbol is implemented as a Python str -Number = (int, float) # A Lisp Number is implemented as a Python int or float - -class Procedure: - "A user-defined Scheme procedure." - def __init__(self, parms, body, env): - self.parms, self.body, self.env = parms, body, env - def __call__(self, *args): - env = ChainMap(dict(zip(self.parms, args)), self.env) - for exp in self.body: - result = evaluate(exp, env) - return result - - -################ Global Environment - -def standard_env(): - "An environment with some Scheme standard procedures." - env = {} - env.update(vars(math)) # sin, cos, sqrt, pi, ... - env.update({ - '+':op.add, '-':op.sub, '*':op.mul, '/':op.truediv, - '>':op.gt, '<':op.lt, '>=':op.ge, '<=':op.le, '=':op.eq, - 'abs': abs, - 'append': op.add, - 'apply': lambda proc, args: proc(*args), - 'begin': lambda *x: x[-1], - 'car': lambda x: x[0], - 'cdr': lambda x: x[1:], - 'cons': lambda x,y: [x] + y, - 'eq?': op.is_, - 'equal?': op.eq, - 'length': len, - 'list': lambda *x: list(x), - 'list?': lambda x: isinstance(x,list), - 'map': lambda *args: list(map(*args)), - 'max': max, - 'min': min, - 'not': op.not_, - 'null?': lambda x: x == [], - 'number?': lambda x: isinstance(x, Number), - 'procedure?': callable, - 'round': round, - 'symbol?': lambda x: isinstance(x, Symbol), - }) - return env - -################ Parsing: parse, tokenize, and read_from_tokens - -def parse(program): - "Read a Scheme expression from a string." - return read_from_tokens(tokenize(program)) - -def tokenize(s): - "Convert a string into a list of tokens." - return s.replace('(',' ( ').replace(')',' ) ').split() - -def read_from_tokens(tokens): - "Read an expression from a sequence of tokens." - if len(tokens) == 0: - raise SyntaxError('unexpected EOF while reading') - token = tokens.pop(0) - if '(' == token: - exp = [] - while tokens[0] != ')': - exp.append(read_from_tokens(tokens)) - tokens.pop(0) # discard ')' - return exp - elif ')' == token: - raise SyntaxError('unexpected )') - else: - return parse_atom(token) - - -def parse_atom(token: str): - "Numbers become numbers; every other token is a symbol." - try: - return int(token) - except ValueError: - try: - return float(token) - except ValueError: - return Symbol(token) - - -################ Interaction: A REPL - -def repl(prompt: str = 'lis.py> ') -> None: - "A prompt-read-eval-print loop." - global_env = standard_env() - while True: - val = evaluate(parse(input(prompt)), global_env) - if val is not None: - print(lispstr(val)) - - -def lispstr(exp): - "Convert a Python object back into a Lisp-readable string." - if isinstance(exp, list): - return '(' + ' '.join(map(lispstr, exp)) + ')' - else: - return str(exp) - - -################ eval - -def evaluate(x, env): - "Evaluate an expression in an environment." - if isinstance(x, Symbol): # variable reference - return env[x] - elif not isinstance(x, list): # constant literal - return x - elif x[0] == 'quote': # (quote exp) - (_, exp) = x - return exp - elif x[0] == 'if': # (if test conseq alt) - (_, test, conseq, alt) = x - exp = (conseq if evaluate(test, env) else alt) - return evaluate(exp, env) - elif x[0] == 'define': # (define var exp) - (_, var, exp) = x - env[var] = evaluate(exp, env) - elif x[0] == 'lambda': # (lambda (var...) body) - (_, parms, *body) = x - return Procedure(parms, body, env) - else: # (proc arg...) - proc = evaluate(x[0], env) - args = [evaluate(exp, env) for exp in x[1:]] - return proc(*args) diff --git a/02-array-seq/lispy/py3.9-no-hints/lis_test.py b/02-array-seq/lispy/py3.9-no-hints/lis_test.py deleted file mode 100644 index f5f4f8e..0000000 --- a/02-array-seq/lispy/py3.9-no-hints/lis_test.py +++ /dev/null @@ -1,166 +0,0 @@ -from pytest import mark, fixture - -from lis import parse, evaluate, standard_env - -############################################################# tests for parse - -@mark.parametrize( 'source, expected', [ - ('7', 7), - ('x', 'x'), - ('(sum 1 2 3)', ['sum', 1, 2, 3]), - ('(+ (* 2 100) (* 1 10))', ['+', ['*', 2, 100], ['*', 1, 10]]), - ('99 100', 99), # parse stops at the first complete expression - ('(a)(b)', ['a']), -]) -def test_parse(source: str, expected): - got = parse(source) - assert got == expected - - -########################################################## tests for evaluate - -# Norvig's tests are not isolated: they assume the -# same environment from first to last test. -global_env_for_first_test = standard_env() - -@mark.parametrize( 'source, expected', [ - ("(quote (testing 1 (2.0) -3.14e159))", ['testing', 1, [2.0], -3.14e159]), - ("(+ 2 2)", 4), - ("(+ (* 2 100) (* 1 10))", 210), - ("(if (> 6 5) (+ 1 1) (+ 2 2))", 2), - ("(if (< 6 5) (+ 1 1) (+ 2 2))", 4), - ("(define x 3)", None), - ("x", 3), - ("(+ x x)", 6), - ("((lambda (x) (+ x x)) 5)", 10), - ("(define twice (lambda (x) (* 2 x)))", None), - ("(twice 5)", 10), - ("(define compose (lambda (f g) (lambda (x) (f (g x)))))", None), - ("((compose list twice) 5)", [10]), - ("(define repeat (lambda (f) (compose f f)))", None), - ("((repeat twice) 5)", 20), - ("((repeat (repeat twice)) 5)", 80), - ("(define fact (lambda (n) (if (<= n 1) 1 (* n (fact (- n 1))))))", None), - ("(fact 3)", 6), - ("(fact 50)", 30414093201713378043612608166064768844377641568960512000000000000), - ("(define abs (lambda (n) ((if (> n 0) + -) 0 n)))", None), - ("(list (abs -3) (abs 0) (abs 3))", [3, 0, 3]), - ("""(define combine (lambda (f) - (lambda (x y) - (if (null? x) (quote ()) - (f (list (car x) (car y)) - ((combine f) (cdr x) (cdr y)))))))""", None), - ("(define zip (combine cons))", None), - ("(zip (list 1 2 3 4) (list 5 6 7 8))", [[1, 5], [2, 6], [3, 7], [4, 8]]), - ("""(define riff-shuffle (lambda (deck) - (begin - (define take (lambda (n seq) (if (<= n 0) (quote ()) (cons (car seq) (take (- n 1) (cdr seq)))))) - (define drop (lambda (n seq) (if (<= n 0) seq (drop (- n 1) (cdr seq))))) - (define mid (lambda (seq) (/ (length seq) 2))) - ((combine append) (take (mid deck) deck) (drop (mid deck) deck)))))""", None), - ("(riff-shuffle (list 1 2 3 4 5 6 7 8))", [1, 5, 2, 6, 3, 7, 4, 8]), - ("((repeat riff-shuffle) (list 1 2 3 4 5 6 7 8))", [1, 3, 5, 7, 2, 4, 6, 8]), - ("(riff-shuffle (riff-shuffle (riff-shuffle (list 1 2 3 4 5 6 7 8))))", [1,2,3,4,5,6,7,8]), -]) -def test_evaluate(source, expected): - got = evaluate(parse(source), global_env_for_first_test) - assert got == expected - - -@fixture -def std_env(): - return standard_env() - -# tests for each of the cases in evaluate - -def test_evaluate_variable(): - env = dict(x=10) - source = 'x' - expected = 10 - got = evaluate(parse(source), env) - assert got == expected - - -def test_evaluate_literal(std_env): - source = '3.3' - expected = 3.3 - got = evaluate(parse(source), std_env) - assert got == expected - - -def test_evaluate_quote(std_env): - source = '(quote (1.1 is not 1))' - expected = [1.1, 'is', 'not', 1] - got = evaluate(parse(source), std_env) - assert got == expected - - -def test_evaluate_if_true(std_env) -> None: - source = '(if 1 10 no-such-thing)' - expected = 10 - got = evaluate(parse(source), std_env) - assert got == expected - - -def test_evaluate_if_false(std_env) -> None: - source = '(if 0 no-such-thing 20)' - expected = 20 - got = evaluate(parse(source), std_env) - assert got == expected - - -def test_define(std_env) -> None: - source = '(define answer (* 6 7))' - got = evaluate(parse(source), std_env) - assert got is None - assert std_env['answer'] == 42 - - -def test_lambda(std_env) -> None: - source = '(lambda (a b) (if (>= a b) a b))' - func = evaluate(parse(source), std_env) - assert func.parms == ['a', 'b'] - assert func.body == [['if', ['>=', 'a', 'b'], 'a', 'b']] - assert func.env is std_env - assert func(1, 2) == 2 - assert func(3, 2) == 3 - - -def test_begin(std_env) -> None: - source = """ - (begin - (define x (* 2 3)) - (* x 7) - ) - """ - got = evaluate(parse(source), std_env) - assert got == 42 - - -def test_invocation_builtin_car(std_env) -> None: - source = '(car (quote (11 22 33)))' - got = evaluate(parse(source), std_env) - assert got == 11 - - -def test_invocation_builtin_append(std_env) -> None: - source = '(append (quote (a b)) (quote (c d)))' - got = evaluate(parse(source), std_env) - assert got == ['a', 'b', 'c', 'd'] - - -def test_invocation_builtin_map(std_env) -> None: - source = '(map (lambda (x) (* x 2)) (quote (1 2 3))))' - got = evaluate(parse(source), std_env) - assert got == [2, 4, 6] - - -def test_invocation_user_procedure(std_env): - source = """ - (begin - (define max (lambda (a b) (if (>= a b) a b))) - (max 22 11) - ) - """ - got = evaluate(parse(source), std_env) - assert got == 22 diff --git a/05-data-classes/dataclass/hackerclub.py b/05-data-classes/dataclass/hackerclub.py index 762c2cd..60f8234 100644 --- a/05-data-classes/dataclass/hackerclub.py +++ b/05-data-classes/dataclass/hackerclub.py @@ -6,7 +6,7 @@ >>> anna HackerClubMember(name='Anna Ravenscroft', guests=[], handle='AnnaRaven') -If ``handle`` is ommitted, it's set to the first part of the member's name:: +If ``handle`` is omitted, it's set to the first part of the member's name:: >>> leo = HackerClubMember('Leo Rochael') >>> leo diff --git a/05-data-classes/dataclass/hackerclub_annotated.py b/05-data-classes/dataclass/hackerclub_annotated.py index 2394796..0b87c50 100644 --- a/05-data-classes/dataclass/hackerclub_annotated.py +++ b/05-data-classes/dataclass/hackerclub_annotated.py @@ -6,7 +6,7 @@ >>> anna HackerClubMember(name='Anna Ravenscroft', guests=[], handle='AnnaRaven') -If ``handle`` is ommitted, it's set to the first part of the member's name:: +If ``handle`` is omitted, it's set to the first part of the member's name:: >>> leo = HackerClubMember('Leo Rochael') >>> leo diff --git a/17-it-generator/aritprog_v3.py b/17-it-generator/aritprog_v3.py index 3dd8e18..914f5c9 100644 --- a/17-it-generator/aritprog_v3.py +++ b/17-it-generator/aritprog_v3.py @@ -5,7 +5,7 @@ import itertools def aritprog_gen(begin, step, end=None): first = type(begin + step)(begin) ap_gen = itertools.count(first, step) - if end is not None: - ap_gen = itertools.takewhile(lambda n: n < end, ap_gen) - return ap_gen + if end is None: + return ap_gen + return itertools.takewhile(lambda n: n < end, ap_gen) # end::ARITPROG_ITERTOOLS[] diff --git a/17-it-generator/coroaverager.py b/17-it-generator/coroaverager.py new file mode 100644 index 0000000..d976760 --- /dev/null +++ b/17-it-generator/coroaverager.py @@ -0,0 +1,43 @@ +""" +A coroutine to compute a running average + +# tag::CORO_AVERAGER_TEST[] + >>> coro_avg = averager() # <1> + >>> next(coro_avg) # <2> + 0.0 + >>> coro_avg.send(10) # <3> + 10.0 + >>> coro_avg.send(30) + 20.0 + >>> coro_avg.send(5) + 15.0 + +# end::CORO_AVERAGER_TEST[] +# tag::CORO_AVERAGER_TEST_CONT[] + + >>> coro_avg.send(20) # <1> + 16.25 + >>> coro_avg.close() # <2> + >>> coro_avg.close() # <3> + >>> coro_avg.send(5) # <4> + Traceback (most recent call last): + ... + StopIteration + +# end::CORO_AVERAGER_TEST_CONT[] + +""" + +# tag::CORO_AVERAGER[] +from collections.abc import Generator + +def averager() -> Generator[float, float, None]: # <1> + total = 0.0 + count = 0 + average = 0.0 + while True: # <2> + term = yield average # <3> + total += term + count += 1 + average = total/count +# end::CORO_AVERAGER[] diff --git a/17-it-generator/coroaverager2.py b/17-it-generator/coroaverager2.py new file mode 100644 index 0000000..3a15f5e --- /dev/null +++ b/17-it-generator/coroaverager2.py @@ -0,0 +1,96 @@ +""" +A coroutine to compute a running average. + +Testing ``averager2`` by itself:: + +# tag::RETURNING_AVERAGER_DEMO_1[] + + >>> coro_avg = averager2() + >>> next(coro_avg) + >>> coro_avg.send(10) # <1> + >>> coro_avg.send(30) + >>> coro_avg.send(6.5) + >>> coro_avg.close() # <2> + +# end::RETURNING_AVERAGER_DEMO_1[] + +Catching `StopIteration` to extract the value returned by +the coroutine:: + +# tag::RETURNING_AVERAGER_DEMO_2[] + + >>> coro_avg = averager2() + >>> next(coro_avg) + >>> coro_avg.send(10) + >>> coro_avg.send(30) + >>> coro_avg.send(6.5) + >>> try: + ... coro_avg.send(STOP) # <1> + ... except StopIteration as exc: + ... result = exc.value # <2> + ... + >>> result # <3> + Result(count=3, average=15.5) + +# end::RETURNING_AVERAGER_DEMO_2[] + +Using `yield from`: + + +# tag::RETURNING_AVERAGER_DEMO_3[] + + >>> def compute(): + ... res = yield from averager2(True) # <1> + ... print('computed:', res) # <2> + ... return res # <3> + ... + >>> comp = compute() # <4> + >>> for v in [None, 10, 20, 30, STOP]: # <5> + ... try: + ... comp.send(v) # <6> + ... except StopIteration as exc: # <7> + ... result = exc.value + received: 10 + received: 20 + received: 30 + received: + computed: Result(count=3, average=20.0) + >>> result # <8> + Result(count=3, average=20.0) + +# end::RETURNING_AVERAGER_DEMO_3[] +""" + +# tag::RETURNING_AVERAGER_TOP[] +from collections.abc import Generator +from typing import Union, NamedTuple + +class Result(NamedTuple): # <1> + count: int # type: ignore # <2> + average: float + +class Sentinel: # <3> + def __repr__(self): + return f'' + +STOP = Sentinel() # <4> + +SendType = Union[float, Sentinel] # <5> +# end::RETURNING_AVERAGER_TOP[] +# tag::RETURNING_AVERAGER[] +def averager2(verbose: bool = False) -> Generator[None, SendType, Result]: # <1> + total = 0.0 + count = 0 + average = 0.0 + while True: + term = yield # <2> + if verbose: + print('received:', term) + if isinstance(term, Sentinel): # <3> + break + total += term # <4> + count += 1 + average = total / count + return Result(count, average) # <5> + +# end::RETURNING_AVERAGER[] diff --git a/17-it-generator/fibo_gen.py b/17-it-generator/fibo_gen.py new file mode 100644 index 0000000..d7296ac --- /dev/null +++ b/17-it-generator/fibo_gen.py @@ -0,0 +1,7 @@ +from collections.abc import Iterator + +def fibonacci() -> Iterator[int]: + a, b = 0, 1 + while True: + yield a + a, b = b, a + b diff --git a/17-it-generator/sentence_gen.py b/17-it-generator/sentence_gen.py index 3dbc606..1f7ebeb 100644 --- a/17-it-generator/sentence_gen.py +++ b/17-it-generator/sentence_gen.py @@ -21,7 +21,7 @@ class Sentence: def __iter__(self): for word in self.words: # <1> yield word # <2> - return # <3> + # <3> # done! <4> diff --git a/18-with-match/lisplus/examples_test.py b/18-with-match/lisplus/examples_test.py deleted file mode 100644 index 38510a3..0000000 --- a/18-with-match/lisplus/examples_test.py +++ /dev/null @@ -1,109 +0,0 @@ -""" -Doctests for `parse`: - ->>> from lis import parse - -# tag::PARSE_DEMO[] ->>> parse('1.5') # <1> -1.5 ->>> parse('set!') # <2> -'set!' ->>> parse('(gcd 18 44)') # <3> -['gcd', 18, 44] ->>> parse('(- m (* n (// m n)))') # <4> -['-', 'm', ['*', 'n', ['//', 'm', 'n']]] - -# end::PARSE_DEMO[] - -""" - -import math - -from lis import run - - -fact_src = """ -(define (! n) - (if (< n 2) - 1 - (* n (! (- n 1))) - ) -) -(! 42) -""" -def test_factorial(): - got = run(fact_src) - assert got == 1405006117752879898543142606244511569936384000000000 - assert got == math.factorial(42) - - -gcd_src = """ -(define (mod m n) - (- m (* n (// m n)))) -(define (gcd m n) - (if (= n 0) - m - (gcd n (mod m n)))) -(gcd 18 45) -""" -def test_gcd(): - got = run(gcd_src) - assert got == 9 - - -quicksort_src = """ -(define (quicksort lst) - (if (null? lst) - lst - (begin - (define pivot (car lst)) - (define rest (cdr lst)) - (append - (quicksort - (filter (lambda (x) (< x pivot)) rest)) - (list pivot) - (quicksort - (filter (lambda (x) (>= x pivot)) rest))) - ) - ) -) -(quicksort (list 2 1 6 3 4 0 8 9 7 5)) -""" -def test_quicksort(): - got = run(quicksort_src) - assert got == [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] - - -# Example from Structure and Interpretation of Computer Programs -# https://mitpress.mit.edu/sites/default/files/sicp/full-text/sicp/book/node12.html - -newton_src = """ -(define (sqrt x) - (sqrt-iter 1.0 x)) -(define (sqrt-iter guess x) - (if (good-enough? guess x) - guess - (sqrt-iter (improve guess x) x))) -(define (good-enough? guess x) - (< (abs (- (* guess guess) x)) 0.001)) -(define (improve guess x) - (average guess (/ x guess))) -(define (average x y) - (/ (+ x y) 2)) -(sqrt 123454321) -""" -def test_newton(): - got = run(newton_src) - assert math.isclose(got, 11111) - - -closure_src = """ -(define (make-adder increment) - (lambda (x) (+ increment x)) -) -(define inc (make-adder 1)) -(inc 99) -""" -def test_newton(): - got = run(closure_src) - assert got == 100 diff --git a/18-with-match/lisplus/lis.py b/18-with-match/lisplus/lis.py deleted file mode 100644 index 7ae0a9d..0000000 --- a/18-with-match/lisplus/lis.py +++ /dev/null @@ -1,192 +0,0 @@ -################ Lispy: Scheme Interpreter in Python 3.10 - -## (c) Peter Norvig, 2010-18; See http://norvig.com/lispy.html -## Minor edits for Fluent Python, Second Edition (O'Reilly, 2021) -## by Luciano Ramalho, adding type hints and pattern matching. - -################ imports and types - -import math -import operator as op -from collections import ChainMap -from collections.abc import MutableMapping, Iterator -from itertools import chain -from typing import Any, TypeAlias - -Symbol: TypeAlias = str -Atom: TypeAlias = float | int | Symbol -Expression: TypeAlias = Atom | list - -Environment: TypeAlias = MutableMapping[Symbol, object] - - -class Procedure: - "A user-defined Scheme procedure." - - def __init__(self, parms: list[Symbol], body: list[Expression], env: Environment): - self.parms = parms - self.body = body - self.env = env - - def __call__(self, *args: Expression) -> Any: - local_env = dict(zip(self.parms, args)) - env: Environment = ChainMap(local_env, self.env) - for exp in self.body: - result = evaluate(exp, env) - return result - - -################ global environment - - -def standard_env() -> Environment: - "An environment with some Scheme standard procedures." - env: Environment = {} - env.update(vars(math)) # sin, cos, sqrt, pi, ... - env.update({ - '+': op.add, - '-': op.sub, - '*': op.mul, - '/': op.truediv, - '//': op.floordiv, - '>': op.gt, - '<': op.lt, - '>=': op.ge, - '<=': op.le, - '=': op.eq, - 'abs': abs, - 'append': lambda *args: list(chain(*args)), - 'apply': lambda proc, args: proc(*args), - 'begin': lambda *x: x[-1], - 'car': lambda x: x[0], - 'cdr': lambda x: x[1:], - 'cons': lambda x, y: [x] + y, - 'eq?': op.is_, - 'equal?': op.eq, - 'filter': lambda *args: list(filter(*args)), - 'length': len, - 'list': lambda *x: list(x), - 'list?': lambda x: isinstance(x, list), - 'map': lambda *args: list(map(*args)), - 'max': max, - 'min': min, - 'not': op.not_, - 'null?': lambda x: x == [], - 'number?': lambda x: isinstance(x, (int, float)), - 'procedure?': callable, - 'round': round, - 'symbol?': lambda x: isinstance(x, Symbol), - }) - return env - - -################ parse, tokenize, and read_from_tokens - - -def parse(program: str) -> Expression: - "Read a Scheme expression from a string." - return read_from_tokens(tokenize(program)) - - -def tokenize(s: str) -> list[str]: - "Convert a string into a list of tokens." - return s.replace('(', ' ( ').replace(')', ' ) ').split() - - -def read_from_tokens(tokens: list[str]) -> Expression: - "Read an expression from a sequence of tokens." - if len(tokens) == 0: - raise SyntaxError('unexpected EOF while reading') - token = tokens.pop(0) - if '(' == token: - exp = [] - while tokens[0] != ')': - exp.append(read_from_tokens(tokens)) - tokens.pop(0) # discard ')' - return exp - elif ')' == token: - raise SyntaxError('unexpected )') - else: - return parse_atom(token) - - -def parse_atom(token: str) -> Atom: - "Numbers become numbers; every other token is a symbol." - try: - return int(token) - except ValueError: - try: - return float(token) - except ValueError: - return Symbol(token) - - -################ interaction: a REPL - - -def repl(prompt: str = 'lis.py> ') -> None: - "A prompt-read-evaluate-print loop." - global_env: Environment = standard_env() - while True: - val = evaluate(parse(input(prompt)), global_env) - if val is not None: - print(lispstr(val)) - - -def lispstr(exp: object) -> str: - "Convert a Python object back into a Lisp-readable string." - if isinstance(exp, list): - return '(' + ' '.join(map(lispstr, exp)) + ')' - else: - return str(exp) - - -################ eval - -# tag::EVALUATE[] -def evaluate(exp: Expression, env: Environment) -> Any: - "Evaluate an expression in an environment." - match exp: - case int(x) | float(x): - return x - case Symbol(var): - return env[var] - case []: - return [] - case ['quote', exp]: - return exp - case ['if', test, consequence, alternative]: - if evaluate(test, env): - return evaluate(consequence, env) - else: - return evaluate(alternative, env) - case ['define', Symbol(var), value_exp]: - env[var] = evaluate(value_exp, env) - case ['define', [Symbol(name), *parms], *body]: - env[name] = Procedure(parms, body, env) - case ['lambda', [*parms], *body]: - return Procedure(parms, body, env) - case [op, *args]: - proc = evaluate(op, env) - values = [evaluate(arg, env) for arg in args] - return proc(*values) - case _: - raise SyntaxError(repr(exp)) -# end::EVALUATE[] - - -################ non-interactive execution - - -def run_lines(source: str) -> Iterator[Any]: - global_env: Environment = standard_env() - tokens = tokenize(source) - while tokens: - exp = read_from_tokens(tokens) - yield evaluate(exp, global_env) - - -def run(source: str) -> Any: - for result in run_lines(source): - pass - return result diff --git a/18-with-match/lisplus/lis_test.py b/18-with-match/lisplus/lis_test.py deleted file mode 100644 index 3688888..0000000 --- a/18-with-match/lisplus/lis_test.py +++ /dev/null @@ -1,182 +0,0 @@ -from typing import Optional - -from pytest import mark, fixture - -from lis import parse, evaluate, Expression, Environment, standard_env - -############################################################# tests for parse - -@mark.parametrize( 'source, expected', [ - ('7', 7), - ('x', 'x'), - ('(sum 1 2 3)', ['sum', 1, 2, 3]), - ('(+ (* 2 100) (* 1 10))', ['+', ['*', 2, 100], ['*', 1, 10]]), - ('99 100', 99), # parse stops at the first complete expression - ('(a)(b)', ['a']), -]) -def test_parse(source: str, expected: Expression) -> None: - got = parse(source) - assert got == expected - - -########################################################## tests for evaluate - -# Norvig's tests are not isolated: they assume the -# same environment from first to last test. -global_env_for_first_test = standard_env() - -@mark.parametrize( 'source, expected', [ - ("(quote (testing 1 (2.0) -3.14e159))", ['testing', 1, [2.0], -3.14e159]), - ("(+ 2 2)", 4), - ("(+ (* 2 100) (* 1 10))", 210), - ("(if (> 6 5) (+ 1 1) (+ 2 2))", 2), - ("(if (< 6 5) (+ 1 1) (+ 2 2))", 4), - ("(define x 3)", None), - ("x", 3), - ("(+ x x)", 6), - ("((lambda (x) (+ x x)) 5)", 10), - ("(define twice (lambda (x) (* 2 x)))", None), - ("(twice 5)", 10), - ("(define compose (lambda (f g) (lambda (x) (f (g x)))))", None), - ("((compose list twice) 5)", [10]), - ("(define repeat (lambda (f) (compose f f)))", None), - ("((repeat twice) 5)", 20), - ("((repeat (repeat twice)) 5)", 80), - ("(define fact (lambda (n) (if (<= n 1) 1 (* n (fact (- n 1))))))", None), - ("(fact 3)", 6), - ("(fact 50)", 30414093201713378043612608166064768844377641568960512000000000000), - ("(define abs (lambda (n) ((if (> n 0) + -) 0 n)))", None), - ("(list (abs -3) (abs 0) (abs 3))", [3, 0, 3]), - ("""(define combine (lambda (f) - (lambda (x y) - (if (null? x) (quote ()) - (f (list (car x) (car y)) - ((combine f) (cdr x) (cdr y)))))))""", None), - ("(define zip (combine cons))", None), - ("(zip (list 1 2 3 4) (list 5 6 7 8))", [[1, 5], [2, 6], [3, 7], [4, 8]]), - ("""(define riff-shuffle (lambda (deck) - (begin - (define take (lambda (n seq) (if (<= n 0) (quote ()) (cons (car seq) (take (- n 1) (cdr seq)))))) - (define drop (lambda (n seq) (if (<= n 0) seq (drop (- n 1) (cdr seq))))) - (define mid (lambda (seq) (/ (length seq) 2))) - ((combine append) (take (mid deck) deck) (drop (mid deck) deck)))))""", None), - ("(riff-shuffle (list 1 2 3 4 5 6 7 8))", [1, 5, 2, 6, 3, 7, 4, 8]), - ("((repeat riff-shuffle) (list 1 2 3 4 5 6 7 8))", [1, 3, 5, 7, 2, 4, 6, 8]), - ("(riff-shuffle (riff-shuffle (riff-shuffle (list 1 2 3 4 5 6 7 8))))", [1,2,3,4,5,6,7,8]), -]) -def test_evaluate(source: str, expected: Optional[Expression]) -> None: - got = evaluate(parse(source), global_env_for_first_test) - assert got == expected - - -@fixture -def std_env() -> Environment: - return standard_env() - -# tests for each of the cases in evaluate - -def test_evaluate_variable() -> None: - env: Environment = dict(x=10) - source = 'x' - expected = 10 - got = evaluate(parse(source), env) - assert got == expected - - -def test_evaluate_literal(std_env: Environment) -> None: - source = '3.3' - expected = 3.3 - got = evaluate(parse(source), std_env) - assert got == expected - - -def test_evaluate_quote(std_env: Environment) -> None: - source = '(quote (1.1 is not 1))' - expected = [1.1, 'is', 'not', 1] - got = evaluate(parse(source), std_env) - assert got == expected - - -def test_evaluate_if_true(std_env: Environment) -> None: - source = '(if 1 10 no-such-thing)' - expected = 10 - got = evaluate(parse(source), std_env) - assert got == expected - - -def test_evaluate_if_false(std_env: Environment) -> None: - source = '(if 0 no-such-thing 20)' - expected = 20 - got = evaluate(parse(source), std_env) - assert got == expected - - -def test_define(std_env: Environment) -> None: - source = '(define answer (* 6 7))' - got = evaluate(parse(source), std_env) - assert got is None - assert std_env['answer'] == 42 - - -def test_lambda(std_env: Environment) -> None: - source = '(lambda (a b) (if (>= a b) a b))' - func = evaluate(parse(source), std_env) - assert func.parms == ['a', 'b'] - assert func.body == [['if', ['>=', 'a', 'b'], 'a', 'b']] - assert func.env is std_env - assert func(1, 2) == 2 - assert func(3, 2) == 3 - - -def test_begin(std_env: Environment) -> None: - source = """ - (begin - (define x (* 2 3)) - (* x 7) - ) - """ - got = evaluate(parse(source), std_env) - assert got == 42 - - -def test_invocation_builtin_car(std_env: Environment) -> None: - source = '(car (quote (11 22 33)))' - got = evaluate(parse(source), std_env) - assert got == 11 - - -def test_invocation_builtin_append(std_env: Environment) -> None: - source = '(append (quote (a b)) (quote (c d)))' - got = evaluate(parse(source), std_env) - assert got == ['a', 'b', 'c', 'd'] - - -def test_invocation_builtin_map(std_env: Environment) -> None: - source = '(map (lambda (x) (* x 2)) (quote (1 2 3))))' - got = evaluate(parse(source), std_env) - assert got == [2, 4, 6] - - -def test_invocation_user_procedure(std_env: Environment) -> None: - source = """ - (begin - (define max (lambda (a b) (if (>= a b) a b))) - (max 22 11) - ) - """ - got = evaluate(parse(source), std_env) - assert got == 22 - - -###################################### for py3.10/lis.py only - -def test_define_function(std_env: Environment) -> None: - source = '(define (max a b) (if (>= a b) a b))' - got = evaluate(parse(source), std_env) - assert got is None - max_fn = std_env['max'] - assert max_fn.parms == ['a', 'b'] - assert max_fn.body == [['if', ['>=', 'a', 'b'], 'a', 'b']] - assert max_fn.env is std_env - assert max_fn(1, 2) == 2 - assert max_fn(3, 2) == 3 diff --git a/18-with-match/lisplus/meta_test.py b/18-with-match/lisplus/meta_test.py deleted file mode 100644 index cb3d062..0000000 --- a/18-with-match/lisplus/meta_test.py +++ /dev/null @@ -1,64 +0,0 @@ -import operator as op - -from lis import run - -env_scm = """ -(define standard-env (list - (list (quote +) +) - (list (quote -) -) -)) -standard-env -""" - -def test_env_build(): - got = run(env_scm) - assert got == [['+', op.add], ['-', op.sub]] - -scan_scm = """ -(define l (quote (a b c))) -(define (scan what where) - (if (null? where) - () - (if (eq? what (car where)) - what - (scan what (cdr where)))) -) -""" - -def test_scan(): - source = scan_scm + '(scan (quote a) l )' - got = run(source) - assert got == 'a' - - -def test_scan_not_found(): - source = scan_scm + '(scan (quote z) l )' - got = run(source) - assert got == [] - - -lookup_scm = """ -(define env (list - (list (quote +) +) - (list (quote -) -) -)) -(define (lookup what where) - (if (null? where) - () - (if (eq? what (car (car where))) - (car (cdr (car where))) - (lookup what (cdr where)))) -) -""" - -def test_lookup(): - source = lookup_scm + '(lookup (quote +) env)' - got = run(source) - assert got == op.add - - -def test_lookup_not_found(): - source = lookup_scm + '(lookup (quote z) env )' - got = run(source) - assert got == [] - diff --git a/02-array-seq/lispy/LICENSE b/18-with-match/lispy/LICENSE similarity index 100% rename from 02-array-seq/lispy/LICENSE rename to 18-with-match/lispy/LICENSE diff --git a/02-array-seq/lispy/README.md b/18-with-match/lispy/README.md similarity index 100% rename from 02-array-seq/lispy/README.md rename to 18-with-match/lispy/README.md diff --git a/02-array-seq/lispy/original/LICENSE b/18-with-match/lispy/original/LICENSE similarity index 100% rename from 02-array-seq/lispy/original/LICENSE rename to 18-with-match/lispy/original/LICENSE diff --git a/02-array-seq/lispy/original/README.md b/18-with-match/lispy/original/README.md similarity index 100% rename from 02-array-seq/lispy/original/README.md rename to 18-with-match/lispy/original/README.md diff --git a/02-array-seq/lispy/original/lis.py b/18-with-match/lispy/original/lis.py similarity index 100% rename from 02-array-seq/lispy/original/lis.py rename to 18-with-match/lispy/original/lis.py diff --git a/02-array-seq/lispy/original/lispy.py b/18-with-match/lispy/original/lispy.py similarity index 100% rename from 02-array-seq/lispy/original/lispy.py rename to 18-with-match/lispy/original/lispy.py diff --git a/02-array-seq/lispy/original/lispytest.py b/18-with-match/lispy/original/lispytest.py similarity index 100% rename from 02-array-seq/lispy/original/lispytest.py rename to 18-with-match/lispy/original/lispytest.py diff --git a/18-with-match/lispy/py3.10/examples_test.py b/18-with-match/lispy/py3.10/examples_test.py new file mode 100644 index 0000000..ed4b68a --- /dev/null +++ b/18-with-match/lispy/py3.10/examples_test.py @@ -0,0 +1,259 @@ +""" +Doctests for `parse` +-------------------- + +# tag::PARSE_ATOM[] +>>> from lis import parse +>>> parse('1.5') +1.5 +>>> parse('ni!') +'ni!' + +# end::PARSE_ATOM[] + +# tag::PARSE_LIST[] +>>> parse('(gcd 18 45)') +['gcd', 18, 45] +>>> parse(''' +... (define double +... (lambda (n) +... (* n 2))) +... ''') +['define', 'double', ['lambda', ['n'], ['*', 'n', 2]]] + +# end::PARSE_LIST[] + +Doctest for `Environment` +------------------------- + +# tag::ENVIRONMENT[] +>>> from lis import Environment +>>> outer_env = {'a': 0, 'b': 1} +>>> inner_env = {'a': 2} +>>> env = Environment(inner_env, outer_env) +>>> env['a'] = 111 # <1> +>>> env['c'] = 222 +>>> env +Environment({'a': 111, 'c': 222}, {'a': 0, 'b': 1}) +>>> env.change('b', 333) # <2> +>>> env +Environment({'a': 111, 'c': 222}, {'a': 0, 'b': 333}) + +# end::ENVIRONMENT[] + +Doctests for `evaluate` +----------------------- + +# tag::EVAL_NUMBER[] +>>> from lis import parse, evaluate, standard_env +>>> evaluate(parse('1.5'), {}) +1.5 + +# end::EVAL_NUMBER[] + +# tag::EVAL_SYMBOL[] +>>> from lis import standard_env +>>> evaluate(parse('+'), standard_env()) + +>>> evaluate(parse('ni!'), standard_env()) +Traceback (most recent call last): + ... +KeyError: 'ni!' + +# end::EVAL_SYMBOL[] + + +# tag::EVAL_QUOTE[] +>>> evaluate(parse('(quote no-such-name)'), {}) +'no-such-name' +>>> evaluate(parse('(quote (99 bottles of beer))'), {}) +[99, 'bottles', 'of', 'beer'] +>>> evaluate(parse('(quote (/ 10 0))'), {}) +['/', 10, 0] + +# end::EVAL_QUOTE[] + +# tag::EVAL_IF[] +>>> evaluate(parse('(if (= 3 3) 1 0))'), standard_env()) +1 +>>> evaluate(parse('(if (= 3 4) 1 0))'), standard_env()) +0 + +# end::EVAL_IF[] + + +# tag::EVAL_LAMBDA[] +>>> expr = '(lambda (a b) (* (/ a b) 100))' +>>> f = evaluate(parse(expr), standard_env()) +>>> f # doctest: +ELLIPSIS + +>>> f(15, 20) +75.0 + +# end::EVAL_LAMBDA[] + +# tag::EVAL_DEFINE[] +>>> global_env = standard_env() +>>> evaluate(parse('(define answer (* 7 6))'), global_env) +>>> global_env['answer'] +42 + +# end::EVAL_DEFINE[] + +# tag::EVAL_DEFUN[] +>>> global_env = standard_env() +>>> percent = '(define (% a b) (* (/ a b) 100))' +>>> evaluate(parse(percent), global_env) +>>> global_env['%'] # doctest: +ELLIPSIS + +>>> global_env['%'](170, 200) +85.0 + +# end::EVAL_DEFUN[] + +function call: + +# tag::EVAL_CALL[] +>>> evaluate(parse('(% (* 12 14) (- 500 100))'), global_env) +42.0 + +# end::EVAL_CALL[] + +# tag::EVAL_SYNTAX_ERROR[] +>>> evaluate(parse('(lambda is not like this)'), standard_env()) +Traceback (most recent call last): + ... +SyntaxError: (lambda is not like this) + +# end::EVAL_SYNTAX_ERROR[] + +""" + +import math + +from lis import run + + +fact_src = """ +(define (! n) + (if (< n 2) + 1 + (* n (! (- n 1))) + ) +) +(! 42) +""" +def test_factorial(): + got = run(fact_src) + assert got == 1405006117752879898543142606244511569936384000000000 + assert got == math.factorial(42) + + +gcd_src = """ +(define (mod m n) + (- m (* n (// m n)))) +(define (gcd m n) + (if (= n 0) + m + (gcd n (mod m n)))) +(gcd 18 45) +""" +def test_gcd(): + got = run(gcd_src) + assert got == 9 + + +quicksort_src = """ +(define (quicksort lst) + (if (null? lst) + lst + (begin + (define pivot (car lst)) + (define rest (cdr lst)) + (append + (quicksort + (filter (lambda (x) (< x pivot)) rest)) + (list pivot) + (quicksort + (filter (lambda (x) (>= x pivot)) rest))) + ) + ) +) +(quicksort (list 2 1 6 3 4 0 8 9 7 5)) +""" +def test_quicksort(): + got = run(quicksort_src) + assert got == [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] + + +# Example from Structure and Interpretation of Computer Programs +# https://mitpress.mit.edu/sites/default/files/sicp/full-text/sicp/book/node12.html + +newton_src = """ +(define (sqrt x) + (sqrt-iter 1.0 x)) +(define (sqrt-iter guess x) + (if (good-enough? guess x) + guess + (sqrt-iter (improve guess x) x))) +(define (good-enough? guess x) + (< (abs (- (* guess guess) x)) 0.001)) +(define (improve guess x) + (average guess (/ x guess))) +(define (average x y) + (/ (+ x y) 2)) +(sqrt 123454321) +""" +def test_newton(): + got = run(newton_src) + assert math.isclose(got, 11111) + + +closure_src = """ +(define (make-adder increment) + (lambda (x) (+ increment x)) +) +(define inc (make-adder 1)) +(inc 99) +""" +def test_newton(): + got = run(closure_src) + assert got == 100 + +closure_with_change_src = """ +(define (make-counter) + (define n 0) + (lambda () + (set! n (+ n 1)) + n) +) +(define counter (make-counter)) +(counter) +(counter) +(counter) +""" +def test_closure_with_change(): + got = run(closure_with_change_src) + assert got == 3 + + +# tag::RUN_AVERAGER[] +closure_averager_src = """ +(define (make-averager) + (define count 0) + (define total 0) + (lambda (new-value) + (set! count (+ count 1)) + (set! total (+ total new-value)) + (/ total count) + ) +) +(define avg (make-averager)) +(avg 10) +(avg 11) +(avg 15) +""" +def test_closure_averager(): + got = run(closure_averager_src) + assert got == 12.0 +# end::RUN_AVERAGER[] \ No newline at end of file diff --git a/02-array-seq/lispy/py3.10/lis.py b/18-with-match/lispy/py3.10/lis.py old mode 100644 new mode 100755 similarity index 70% rename from 02-array-seq/lispy/py3.10/lis.py rename to 18-with-match/lispy/py3.10/lis.py index 035481f..4a5195c --- a/02-array-seq/lispy/py3.10/lis.py +++ b/18-with-match/lispy/py3.10/lis.py @@ -1,46 +1,82 @@ +#!/usr/bin/env python + ################ Lispy: Scheme Interpreter in Python 3.10 ## (c) Peter Norvig, 2010-18; See http://norvig.com/lispy.html ## Minor edits for Fluent Python, Second Edition (O'Reilly, 2021) ## by Luciano Ramalho, adding type hints and pattern matching. + ################ Imports and Types +# tag::IMPORTS[] import math import operator as op from collections import ChainMap -from collections.abc import MutableMapping, Iterator from itertools import chain -from typing import Any, TypeAlias +from typing import Any, TypeAlias, NoReturn Symbol: TypeAlias = str Atom: TypeAlias = float | int | Symbol Expression: TypeAlias = Atom | list - -Environment: TypeAlias = MutableMapping[Symbol, object] +# end::IMPORTS[] -class Procedure: - "A user-defined Scheme procedure." +################ Parsing: parse, tokenize, and read_from_tokens - def __init__(self, parms: list[Symbol], body: list[Expression], env: Environment): - self.parms = parms - self.body = body - self.env = env +def parse(program: str) -> Expression: + "Read a Scheme expression from a string." + return read_from_tokens(tokenize(program)) - def __call__(self, *args: Expression) -> Any: - local_env = dict(zip(self.parms, args)) - env: Environment = ChainMap(local_env, self.env) - for exp in self.body: - result = evaluate(exp, env) - return result +def tokenize(s: str) -> list[str]: + "Convert a string into a list of tokens." + return s.replace('(', ' ( ').replace(')', ' ) ').split() +def read_from_tokens(tokens: list[str]) -> Expression: + "Read an expression from a sequence of tokens." + if len(tokens) == 0: + raise SyntaxError('unexpected EOF while reading') + token = tokens.pop(0) + if '(' == token: + exp = [] + while tokens[0] != ')': + exp.append(read_from_tokens(tokens)) + tokens.pop(0) # discard ')' + return exp + elif ')' == token: + raise SyntaxError('unexpected )') + else: + return parse_atom(token) + +def parse_atom(token: str) -> Atom: + "Numbers become numbers; every other token is a symbol." + try: + return int(token) + except ValueError: + try: + return float(token) + except ValueError: + return Symbol(token) ################ Global Environment +# tag::ENV_CLASS[] +class Environment(ChainMap): + "A ChainMap that allows changing an item in-place." + + def change(self, key: Symbol, value: object) -> None: + "Find where key is defined and change the value there." + for map in self.maps: + if key in map: + map[key] = value + return + raise KeyError(key) +# end::ENV_CLASS[] + + def standard_env() -> Environment: "An environment with some Scheme standard procedures." - env: Environment = {} + env = Environment() env.update(vars(math)) # sin, cos, sqrt, pi, ... env.update({ '+': op.add, @@ -60,6 +96,7 @@ def standard_env() -> Environment: 'car': lambda x: x[0], 'cdr': lambda x: x[1:], 'cons': lambda x, y: [x] + y, + 'display': lambda x: print(lispstr(x)), 'eq?': op.is_, 'equal?': op.eq, 'filter': lambda *args: list(filter(*args)), @@ -78,68 +115,33 @@ def standard_env() -> Environment: }) return env -################ Parsing: parse, tokenize, and read_from_tokens - -def parse(program: str) -> Expression: - "Read a Scheme expression from a string." - return read_from_tokens(tokenize(program)) - - -def tokenize(s: str) -> list[str]: - "Convert a string into a list of tokens." - return s.replace('(', ' ( ').replace(')', ' ) ').split() - - -def read_from_tokens(tokens: list[str]) -> Expression: - "Read an expression from a sequence of tokens." - if len(tokens) == 0: - raise SyntaxError('unexpected EOF while reading') - token = tokens.pop(0) - if '(' == token: - exp = [] - while tokens[0] != ')': - exp.append(read_from_tokens(tokens)) - tokens.pop(0) # discard ')' - return exp - elif ')' == token: - raise SyntaxError('unexpected )') - else: - return parse_atom(token) - - -def parse_atom(token: str) -> Atom: - "Numbers become numbers; every other token is a symbol." - try: - return int(token) - except ValueError: - try: - return float(token) - except ValueError: - return Symbol(token) - ################ Interaction: A REPL -def repl(prompt: str = 'lis.py> ') -> None: - "A prompt-read-evaluate-print loop." +# tag::REPL[] +def repl() -> NoReturn: + "A prompt-read-eval-print loop." global_env = standard_env() while True: - val = evaluate(parse(input(prompt)), global_env) + ast = parse(input('lis.py> ')) + val = evaluate(ast, global_env) if val is not None: print(lispstr(val)) - def lispstr(exp: object) -> str: "Convert a Python object back into a Lisp-readable string." if isinstance(exp, list): return '(' + ' '.join(map(lispstr, exp)) + ')' else: return str(exp) +# end::REPL[] -################ eval +################ Evaluator # tag::EVALUATE[] +KEYWORDS = ['quote', 'if', 'lambda', 'define', 'set!'] + def evaluate(exp: Expression, env: Environment) -> Any: "Evaluate an expression in an environment." match exp: @@ -149,40 +151,66 @@ def evaluate(exp: Expression, env: Environment) -> Any: return env[var] case []: return [] - case ['quote', exp]: - return exp + case ['quote', x]: + return x case ['if', test, consequence, alternative]: if evaluate(test, env): return evaluate(consequence, env) else: return evaluate(alternative, env) + case ['lambda', [*parms], *body] if body: + return Procedure(parms, body, env) case ['define', Symbol(var), value_exp]: env[var] = evaluate(value_exp, env) - case ['define', [Symbol(name), *parms], *body]: + case ['define', [Symbol(name), *parms], *body] if body: env[name] = Procedure(parms, body, env) - case ['lambda', [*parms], *body]: - return Procedure(parms, body, env) - case [op, *args]: + case ['set!', Symbol(var), value_exp]: + env.change(var, evaluate(value_exp, env)) + case [op, *args] if op not in KEYWORDS: proc = evaluate(op, env) values = [evaluate(arg, env) for arg in args] return proc(*values) case _: - raise SyntaxError(repr(exp)) + raise SyntaxError(lispstr(exp)) # end::EVALUATE[] +# tag::PROCEDURE[] +class Procedure: + "A user-defined Scheme procedure." -################ non-interactive execution + def __init__( # <1> + self, parms: list[Symbol], body: list[Expression], env: Environment + ): + self.parms = parms # <2> + self.body = body + self.env = env + + def __call__(self, *args: Expression) -> Any: # <3> + local_env = dict(zip(self.parms, args)) # <4> + env = Environment(local_env, self.env) # <5> + for exp in self.body: # <6> + result = evaluate(exp, env) + return result # <7> +# end::PROCEDURE[] -def run_lines(source: str) -> Iterator[Any]: - global_env: Environment = standard_env() +################ command-line interface + +def run(source: str) -> Any: + global_env = standard_env() tokens = tokenize(source) while tokens: exp = read_from_tokens(tokens) - yield evaluate(exp, global_env) - - -def run(source: str) -> Any: - for result in run_lines(source): - pass + result = evaluate(exp, global_env) return result + +def main(args: list[str]) -> None: + if len(args) == 1: + with open(args[0]) as fp: + run(fp.read()) + else: + repl() + +if __name__ == '__main__': + import sys + main(sys.argv[1:]) diff --git a/02-array-seq/lispy/py3.10/lis_test.py b/18-with-match/lispy/py3.10/lis_test.py similarity index 100% rename from 02-array-seq/lispy/py3.10/lis_test.py rename to 18-with-match/lispy/py3.10/lis_test.py diff --git a/18-with-match/lispy/py3.10/quicksort.scm b/18-with-match/lispy/py3.10/quicksort.scm new file mode 100644 index 0000000..08dd596 --- /dev/null +++ b/18-with-match/lispy/py3.10/quicksort.scm @@ -0,0 +1,17 @@ +(define (quicksort lst) + (if (null? lst) + lst + (begin + (define pivot (car lst)) + (define rest (cdr lst)) + (append + (quicksort + (filter (lambda (x) (< x pivot)) rest)) + (list pivot) + (quicksort + (filter (lambda (x) (>= x pivot)) rest))) + ) + ) +) +(display + (quicksort (list 2 1 6 3 4 0 8 9 7 5))) diff --git a/02-array-seq/lispy/py3.9/README.md b/18-with-match/lispy/py3.9/README.md similarity index 100% rename from 02-array-seq/lispy/py3.9/README.md rename to 18-with-match/lispy/py3.9/README.md diff --git a/02-array-seq/lispy/py3.9/lis.py b/18-with-match/lispy/py3.9/lis.py similarity index 91% rename from 02-array-seq/lispy/py3.9/lis.py rename to 18-with-match/lispy/py3.9/lis.py index 11f4402..9e4dec1 100644 --- a/02-array-seq/lispy/py3.9/lis.py +++ b/18-with-match/lispy/py3.9/lis.py @@ -140,16 +140,18 @@ def evaluate(x: Expression, env: Environment) -> Any: (_, exp) = x return exp elif x[0] == 'if': # (if test conseq alt) - (_, test, conseq, alt) = x - exp = (conseq if evaluate(test, env) else alt) - return evaluate(exp, env) - elif x[0] == 'define': # (define var exp) - (_, var, exp) = x - env[var] = evaluate(exp, env) - elif x[0] == 'lambda': # (lambda (var...) body) + (_, test, consequence, alternative) = x + if evaluate(test, env): + return evaluate(consequence, env) + else: + return evaluate(alternative, env) + elif x[0] == 'define': # (define name exp) + (_, name, exp) = x + env[name] = evaluate(exp, env) + elif x[0] == 'lambda': # (lambda (parm…) body) (_, parms, *body) = x return Procedure(parms, body, env) - else: # (proc arg...) + else: # (proc arg…) proc = evaluate(x[0], env) args = [evaluate(exp, env) for exp in x[1:]] return proc(*args) diff --git a/02-array-seq/lispy/py3.9/lis_test.py b/18-with-match/lispy/py3.9/lis_test.py similarity index 100% rename from 02-array-seq/lispy/py3.9/lis_test.py rename to 18-with-match/lispy/py3.9/lis_test.py diff --git a/18-with-match/mirror.py b/18-with-match/mirror.py index ba31944..841c6ab 100644 --- a/18-with-match/mirror.py +++ b/18-with-match/mirror.py @@ -11,11 +11,11 @@ While active, the context manager reverses text output to ... print('Alice, Kitty and Snowdrop') # <2> ... print(what) ... - pordwonS dna yttiK ,ecilA # <3> + pordwonS dna yttiK ,ecilA YKCOWREBBAJ - >>> what # <4> + >>> what # <3> 'JABBERWOCKY' - >>> print('Back to normal.') # <5> + >>> print('Back to normal.') # <4> Back to normal. # end::MIRROR_DEMO_1[] @@ -27,15 +27,15 @@ This exposes the context manager operation:: >>> from mirror import LookingGlass >>> manager = LookingGlass() # <1> - >>> manager - + >>> manager # doctest: +ELLIPSIS + >>> monster = manager.__enter__() # <2> >>> monster == 'JABBERWOCKY' # <3> eurT >>> monster 'YKCOWREBBAJ' - >>> manager - >ca875a2x0 ta tcejbo ssalGgnikooL.rorrim< + >>> manager # doctest: +ELLIPSIS + >... ta tcejbo ssalGgnikooL.rorrim< >>> manager.__exit__(None, None, None) # <4> >>> monster 'JABBERWOCKY' @@ -69,10 +69,11 @@ The context manager can handle and "swallow" exceptions. # tag::MIRROR_EX[] +import sys + class LookingGlass: def __enter__(self): # <1> - import sys self.original_write = sys.stdout.write # <2> sys.stdout.write = self.reverse_write # <3> return 'JABBERWOCKY' # <4> @@ -81,12 +82,9 @@ class LookingGlass: self.original_write(text[::-1]) def __exit__(self, exc_type, exc_value, traceback): # <6> - import sys # <7> - sys.stdout.write = self.original_write # <8> - if exc_type is ZeroDivisionError: # <9> + sys.stdout.write = self.original_write # <7> + if exc_type is ZeroDivisionError: # <8> print('Please DO NOT divide by zero!') - return True # <10> - # <11> - - + return True # <9> + # <10> # end::MIRROR_EX[] diff --git a/18-with-match/mirror_gen.py b/18-with-match/mirror_gen.py index 457955a..6dfaf02 100644 --- a/18-with-match/mirror_gen.py +++ b/18-with-match/mirror_gen.py @@ -35,22 +35,36 @@ This exposes the context manager operation:: >>> manager # doctest: +ELLIPSIS >...x0 ta tcejbo reganaMtxetnoCrotareneG_.biltxetnoc< >>> manager.__exit__(None, None, None) # <4> + False >>> monster 'JABBERWOCKY' # end::MIRROR_GEN_DEMO_2[] +The decorated generator also works as a decorator: + + +# tag::MIRROR_GEN_DECO[] + >>> @looking_glass() + ... def verse(): + ... print('The time has come') + ... + >>> verse() # <1> + emoc sah emit ehT + >>> print('back to normal') # <2> + back to normal + +# end::MIRROR_GEN_DECO[] + """ # tag::MIRROR_GEN_EX[] - import contextlib - +import sys @contextlib.contextmanager # <1> def looking_glass(): - import sys original_write = sys.stdout.write # <2> def reverse_write(text): # <3> @@ -59,6 +73,4 @@ def looking_glass(): sys.stdout.write = reverse_write # <4> yield 'JABBERWOCKY' # <5> sys.stdout.write = original_write # <6> - - # end::MIRROR_GEN_EX[] diff --git a/18-with-match/mirror_gen_exc.py b/18-with-match/mirror_gen_exc.py index c446918..0c20a3b 100644 --- a/18-with-match/mirror_gen_exc.py +++ b/18-with-match/mirror_gen_exc.py @@ -35,6 +35,7 @@ This exposes the context manager operation:: >>> manager # doctest: +ELLIPSIS >...x0 ta tcejbo reganaMtxetnoCrotareneG_.biltxetnoc< >>> manager.__exit__(None, None, None) # <4> + False >>> monster 'JABBERWOCKY' @@ -48,7 +49,7 @@ is handled by the context manager): # tag::MIRROR_GEN_DEMO_3[] - >>> from mirror_gen import looking_glass + >>> from mirror_gen_exc import looking_glass >>> with looking_glass(): ... print('Humpty Dumpty') ... x = 1/0 # <1> @@ -74,13 +75,11 @@ is handled by the context manager): # tag::MIRROR_GEN_EXC[] - import contextlib - +import sys @contextlib.contextmanager def looking_glass(): - import sys original_write = sys.stdout.write def reverse_write(text): @@ -96,6 +95,4 @@ def looking_glass(): sys.stdout.write = original_write # <3> if msg: print(msg) # <4> - - # end::MIRROR_GEN_EXC[] diff --git a/20-futures/getflags/flags2_asyncio.py b/20-futures/getflags/flags2_asyncio.py index e7a73ac..88a18ed 100755 --- a/20-futures/getflags/flags2_asyncio.py +++ b/20-futures/getflags/flags2_asyncio.py @@ -36,10 +36,10 @@ async def get_flag(session: aiohttp.ClientSession, # <2> resp.raise_for_status() # <3> return bytes() -async def download_one(session: aiohttp.ClientSession, # <4> +async def download_one(session: aiohttp.ClientSession, cc: str, base_url: str, - semaphore: asyncio.Semaphore, + semaphore: asyncio.Semaphore, # <4> verbose: bool) -> Result: try: async with semaphore: # <5> diff --git a/20-futures/getflags/flags3_asyncio.py b/20-futures/getflags/flags3_asyncio.py index 37b5957..04a01e2 100755 --- a/20-futures/getflags/flags3_asyncio.py +++ b/20-futures/getflags/flags3_asyncio.py @@ -36,9 +36,9 @@ async def get_flag(session: aiohttp.ClientSession, return bytes() # tag::FLAGS3_ASYNCIO_GET_COUNTRY[] -async def get_country(session: aiohttp.ClientSession, # <1> +async def get_country(session: aiohttp.ClientSession, base_url: str, - cc: str) -> str: + cc: str) -> str: # <1> url = f'{base_url}/{cc}/metadata.json' async with session.get(url) as resp: if resp.status == 200: diff --git a/20-futures/getflags/requirements.txt b/20-futures/getflags/requirements.txt index 793679b..37baa8d 100644 --- a/20-futures/getflags/requirements.txt +++ b/20-futures/getflags/requirements.txt @@ -1,11 +1,13 @@ -aiohttp==3.7.4 +aiohttp==3.7.4.post0 async-timeout==3.0.1 -attrs==20.3.0 -certifi==2020.12.5 +attrs==21.2.0 +certifi==2021.5.30 chardet==4.0.0 -idna==2.10 -requests==2.25.1 -urllib3==1.26.5 -tqdm==4.56.2 +charset-normalizer==2.0.4 +idna==3.2 multidict==5.1.0 +requests==2.26.0 +tqdm==4.62.2 +typing-extensions==3.10.0.2 +urllib3==1.26.6 yarl==1.6.3 diff --git a/20-futures/getflags/slow_server.py b/20-futures/getflags/slow_server.py index 9d8142b..bede010 100755 --- a/20-futures/getflags/slow_server.py +++ b/20-futures/getflags/slow_server.py @@ -38,6 +38,7 @@ class SlowHTTPRequestHandler(SimpleHTTPRequestHandler): """Serve a GET request.""" time.sleep(.5) if random() < self.error_rate: + # HTTPStatus.IM_A_TEAPOT requires Python >= 3.9 self.send_error(HTTPStatus.IM_A_TEAPOT, "I'm a Teapot") else: f = self.send_head() diff --git a/22-dyn-attr-prop/oscon/demo_schedule2.py b/22-dyn-attr-prop/oscon/demo_schedule2.py deleted file mode 100755 index 12fd440..0000000 --- a/22-dyn-attr-prop/oscon/demo_schedule2.py +++ /dev/null @@ -1,25 +0,0 @@ -#!/usr/bin/env python3 - -import shelve - -from schedule_v2 import DB_NAME, CONFERENCE, load_db -from schedule_v2 import DbRecord, Event - -with shelve.open(DB_NAME) as db: - if CONFERENCE not in db: - load_db(db) - - DbRecord.set_db(db) - event = DbRecord.fetch('event.33950') - print(event) - print(event.venue) - print(event.venue.name) - for spkr in event.speakers: - print(f'{spkr.serial}:', spkr.name) - - print(repr(Event.venue)) - - event2 = DbRecord.fetch('event.33451') - print(event2) - print(event2.fetch) - print(event2.venue) \ No newline at end of file