Testing some tidbits

Wednesday 4 December 2024

I posted a Python tidbit about checking if a string consists entirely of zeros and ones:

Python expressions checking if a string is only zeros and ones

I got a bunch of replies suggesting other ways. I wanted to post those, but I also wanted to check if they were right. A classic testing structure would have required putting them all in functions, etc, which I didn’t want to bother with.

So I cobbled together a test harness for them (also in a gist if you want):

GOOD = [
    "",
    "0",
    "1",
    "000000000000000000",
    "111111111111111111",
    "101000100011110101010000101010101001001010101",
]

BAD = [
    "x",
    "nedbat",
    "x000000000000000000000000000000000000",
    "111111111111111111111111111111111111x",
    "".join(chr(i) for i in range(10000)),
]

TESTS = """
    # The original checks
    all(c in "01" for c in s)
    set(s).issubset({"0", "1"})
    set(s) <= {"0", "1"}
    re.fullmatch(r"[01]*", s)
    s.strip("01") == ""
    not s.strip("01")

    # Using min/max
    "0" <= min(s or "0") <= max(s or "1") <= "1"
    not s or (min(s) in "01" and max(s) in "01")
    ((ss := sorted(s or "0")) and ss[0] in "01" and ss[-1] in "01")

    # Using counting
    s.count("0") + s.count("1") == len(s)
    (not (ctr := Counter(s)) or (ctr["0"] + ctr["1"] == len(s)))

    # Using numeric tests
    all(97*c - c*c > 2351 for c in s.encode())
    max((abs(ord(c) - 48.5) for c in "0"+s)) < 1
    all(map(lambda x: (ord(x) ^ 48) < 2, s))

    # Removing all the 0 and 1
    re.sub(r"[01]", "", s) == ""
    len((s).translate(str.maketrans("", "", "01"))) == 0
    len((s).replace("0", "").replace("1", "")) == 0
    "".join(("1".join((s).split("0"))).split("1")) == ""

    # A few more for good measure
    set(s + "01") == set("01")
    not (set(s) - set("01"))
    not any(filter(lambda x: x not in {"0", "1"}, s))
    all(map(lambda x: x in "01", s))
"""

import re
from collections import Counter
from inspect import cleandoc

g = {
    "re": re,
    "Counter": Counter,
}

for test in cleandoc(TESTS).splitlines():
    test = test.partition("#")[0]
    if not test:
        continue
    for ss, expected in [(GOOD, True), (BAD, False)]:
        for s in ss:
            result = eval(test, {"s": s} | g)
            if bool(result) != expected:
                print("OOPS:")
                print(f"   {s = }")
                print(f"   {test}")
                print(f"   {expected = }")

It’s a good thing I did this because a few of the suggestions needed adjusting, especially for dealing with the empty string. But now they all work, and are checked!

More Python expressions checking if a string is only zeros and ones

BTW, if you prefer Mastodon to BlueSky, the posts are there too: first and second.

Also BTW: Brian Okken adapted these tests to pytest, showing some interesting pytest techniques.

Dinner

Sunday 1 December 2024

My son Nat has autism, and one way it affects him is he can be very quiet and passive, even when he wants something very much. This played out on our drive home from Thanks­giving this week.

Nat loves his routines, and wants to know what is going to happen. We make him a two-week calendar every weekend, laying out what to expect coming up. Thanks­giving was tricky this year for a few reasons. First, it was especially late in the year. In the early weeks of November, he looked at his calendar and said, “Thursday.” We figured out that he meant, “it’s November, so there should be a special Thursday, but I don’t see it here.” I added on an extra row so he could see when Thanks­giving was going to happen.

But there were other complications. After a few rounds of planning, we ended up with two Thanks­givings: the first on Thursday with my sister, which we’ve hardly ever done, and then a second on Friday with Susan’s family, the usual cohort. We’d be staying in a hotel Thursday night.

We tried to carefully keep Nat informed about the plan and talked about it a number of times. He was great with all of it, all the way through the Friday meal. But driving home Friday night, he seemed a little bothered. We asked him, “what’s wrong?” A common answer to that is “no,” either because he’s not sure how to explain, or he’s not sure he’s allowed to question what’s happening, or some other form of passivity. It’s hard to get an answer because if you offer options (“do your feet hurt?”) he might just repeat that even if it isn’t the real problem.

I thought maybe he was concerned about what was going to be happening next, and often going over the routine helps. So we started to review the plan. I asked, “where are we sleeping tonight?” He answered “Brookline.”

“Tomorrow where will you eat breakfast?” — “Brookline.”

“Where will you eat lunch?” — “Brookline.”

“Where will you eat dinner?” — Here we expected he’d name his group home, but instead he said — “dinner.”

Aha! This was the clue we needed. Here’s another tricky thing about Thanks­giving: if you have a meal at 4pm (as we had on Thursday), that counts as dinner. But what if you have a meal at 2pm as we had on Friday? Even if it’s a large meal and you aren’t hungry, by the time it gets dark shouldn’t there be dinner? We didn’t have dinner! This was what was bothering him. We had completely skipped over part of the expected routine. And even with all our planning, we hadn’t thought to explain that Grandma’s big Friday meal was going to be both lunch and dinner.

So we asked, “Do you want to stop somewhere to have dinner?” — “Yes.” So we stopped at McDonald’s for a crispy chicken sandwich (removed from the bun, dipped in sweet & sour sauce), fries and a Sprite. Judging from the noises once we were back in the car, maybe he was stuffing himself on principle, but we were back on the routine, so everyone was happy.

It’s not easy to find out what Nat wants, so when he tells us, even indirectly, we try to give it to him. Some people might have resisted making a stop when we were already late getting home, or having a meal when no one was actually hungry. But it wasn’t difficult and didn’t take long. It was a small thing to do, but felt like a large part of parenting: listening to your children’s needs no matter how quiet, and helping to meet them even when they are different than your own.

My politics

Thursday 21 November 2024

I am a liberal. I believe in helping people. But these days I see cruelty masquerading as righteousness and selfishness labelled as freedom. I see politicians lifting up the powerful and stepping on the already downtrodden.

Hubert Humphrey laid out a rubric I think we are doing poorly at:

The ultimate moral test of any government is the way it treats three groups of its citizens. First, those in the dawn of life — our children. Second, those in the shadows of life — our needy, our sick, our handicapped. Third, those in the twilight of life — our elderly.

Trump’s second term is a huge concern to me. He will cause chaos and turmoil. He will change institutions for the worse. Expertise will be mocked and science will be ignored. Too many people emboldened by his ascendance will give in to their worst impulses.

But I remain optimistic about the future. We will heal and restore. I don’t know how long it will take, but we will. Martin Luther King Jr said:

We shall overcome because the arc of the moral universe is long but it bends toward justice.

I am proud to be an American and I love what America can be at its best. There is too much ugliness in American discourse and behavior today, but America is more than that. I am proud to fly the flag.

US Flag

Loop targets

Tuesday 19 November 2024

I posted a Python tidbit about how for loops can assign to other things than simple variables, and many people were surprised or even concerned:

Sample Python assigning to a dict item in a for loop, same as text below
params = {
    "query": QUERY,
    "page_size": 100,
}

# Get page=0, page=1, page=2, ...
for params["page"] in itertools.count():
    data = requests.get(SEARCH_URL, params).json()
    if not data["results"]:
        break
    ...

This code makes successive GET requests to a URL, with a params dict as the data payload. Each request uses the same data, except the “page” item is 0, then 1, 2, and so on. It has the same effect as if we had written it:

for page_num in itertools.count():
    params["page"] = page_num
    data = requests.get(SEARCH_URL, params).json()

One reply asked if there was a new params dict in each iteration. No, loops in Python do not create a scope, and never make new variables. The loop target is assigned to exactly as if it were an assignment statement.

As a Python Discord helper once described it,

While loops are “if” on repeat. For loops are assignment on repeat.

A loop like for <ANYTHING> in <ITER>: will take successive values from <ITER> and do an assignment exactly as this statement would: <ANYTHING> = <VAL>. If the assignment statement is ok, then the for loop is ok.

We’re used to seeing for loops that do more than a simple assignment:

for i, thing in enumerate(things):
    ...

for x, y, z in zip(xs, ys, zs):
    ...

These work because Python can assign to a number of variables at once:

i, thing = 0, "hello"
x, y, z = 1, 2, 3

Assigning to a dict key (or an attribute, or a property setter, and so on) in a for loop is an example of Python having a few independent mechanisms that combine in uniform ways. We aren’t used to seeing exotic combinations, but you can reason through how they would behave, and you would be right.

You can assign to a dict key in an assignment statement, so you can assign to it in a for loop. You might decide it’s too unusual to use, but it is possible and it works.

Coverage.py originally

Saturday 2 November 2024

Something many people don’t realize is that I didn’t write the original coverage.py. It was written by Gareth Rees in 2001. I’ve been extending and maintaining it since 2004. This ancient history came up this week, so I grabbed the 2001 version from archive.org to keep it here for posterity.

I already had a copy of Gareth’s original page about coverage.py, which now links to my local copy of coverage.py from 2001. BTW: that page is itself a historical artifact now, with the header from this site as it looked when I first copied the page.

The original coverage.py was a single file, so the “coverage.py” name was literal: it was the name of the file. It only had about 350 lines of code, including a few to deal with pre-2.0 Python! Some of those lines remain nearly unchanged to this day, but most of it has been heavily refactored and extended.

Coverage.py now has about 20k lines of Python in about 100 files. The project now has twice the amount of C code as the original file had Python. I guess in almost 20 years a lot can happen!

It’s interesting to see this code again, and to reflect on how far it’s come.

Older: