diff options
| author | Peter Pentchev <roam@ringlet.net> | 2024-09-10 23:49:05 +0300 |
|---|---|---|
| committer | Peter Pentchev <roam@ringlet.net> | 2024-09-11 21:32:24 +0300 |
| commit | 7b0cce752eca91696664c2bb0de3c5e0395f3fda (patch) | |
| tree | 9f9c62c53aecdc669cec6ff05d19a478b0895bf7 /challenge-286/ppentchev/python | |
| parent | cbf28e6e924b7cd24d1614a5ad28d801f0e85dea (diff) | |
| download | perlweeklychallenge-club-7b0cce752eca91696664c2bb0de3c5e0395f3fda.tar.gz perlweeklychallenge-club-7b0cce752eca91696664c2bb0de3c5e0395f3fda.tar.bz2 perlweeklychallenge-club-7b0cce752eca91696664c2bb0de3c5e0395f3fda.zip | |
Add Peter Pentchev's Python solutions to 286
Also fix a couple of things in the other files:
- make the TAP test functions accept an arrayref for the command to run
instead of a single string, since we want to run the Python solution using
`python3 -B -u /path/to/the/program`
- fix the use of Test::More::skip() in the Self Spammer test function
- fix a shadowed variable in the Order Game test function
- correct the indentation in the documentation's "Implementation details" section
- make the README file a copy of docs/index.md instead of a wrong copy of
a completely different project's README file
- point blog.txt to the Ringlet copy of this challenge's documentation
Diffstat (limited to 'challenge-286/ppentchev/python')
| -rw-r--r-- | challenge-286/ppentchev/python/README.md | 8 | ||||
| -rw-r--r-- | challenge-286/ppentchev/python/pyproject.toml | 19 | ||||
| -rw-r--r-- | challenge-286/ppentchev/python/requirements/ruff.txt | 4 | ||||
| -rw-r--r-- | challenge-286/ppentchev/python/ruff-base.toml | 39 | ||||
| -rwxr-xr-x | challenge-286/ppentchev/python/scripts/ch-1.py | 39 | ||||
| -rwxr-xr-x | challenge-286/ppentchev/python/scripts/ch-2.py | 109 | ||||
| -rw-r--r-- | challenge-286/ppentchev/python/tox.ini | 71 |
7 files changed, 289 insertions, 0 deletions
diff --git a/challenge-286/ppentchev/python/README.md b/challenge-286/ppentchev/python/README.md new file mode 100644 index 0000000000..f7121289e5 --- /dev/null +++ b/challenge-286/ppentchev/python/README.md @@ -0,0 +1,8 @@ +<!-- +SPDX-FileCopyrightText: Peter Pentchev <roam@ringlet.net> +SPDX-License-Identifier: BSD-2-Clause +--> + +# pwc-286 - Python solutions to the Perl Weekly Challenge 286 tasks + +See the `ppentchev/docs/index.md` file for details. diff --git a/challenge-286/ppentchev/python/pyproject.toml b/challenge-286/ppentchev/python/pyproject.toml new file mode 100644 index 0000000000..1b9e32ae87 --- /dev/null +++ b/challenge-286/ppentchev/python/pyproject.toml @@ -0,0 +1,19 @@ +# SPDX-FileCopyrightText: Peter Pentchev <roam@ringlet.net> +# SPDX-License-Identifier: BSD-2-Clause + +[tool.mypy] +strict = true + +[tool.ruff] +extend = "ruff-base.toml" +output-format = "concise" +preview = true + +[tool.ruff.lint] +select = ["ALL"] + +[tool.test-stages] +stages = [ + "@check and @quick and not @manual", + "@check and not @manual", +] diff --git a/challenge-286/ppentchev/python/requirements/ruff.txt b/challenge-286/ppentchev/python/requirements/ruff.txt new file mode 100644 index 0000000000..1c69e31418 --- /dev/null +++ b/challenge-286/ppentchev/python/requirements/ruff.txt @@ -0,0 +1,4 @@ +# SPDX-FileCopyrightText: Peter Pentchev <roam@ringlet.net> +# SPDX-License-Identifier: BSD-2-Clause + +ruff == 0.6.4 diff --git a/challenge-286/ppentchev/python/ruff-base.toml b/challenge-286/ppentchev/python/ruff-base.toml new file mode 100644 index 0000000000..03cd7fa385 --- /dev/null +++ b/challenge-286/ppentchev/python/ruff-base.toml @@ -0,0 +1,39 @@ +# SPDX-FileCopyrightText: Peter Pentchev <roam@ringlet.net> +# SPDX-License-Identifier: BSD-2-Clause + +target-version = "py311" +line-length = 100 + +[lint] +select = [] +ignore = [ + # No blank lines before the class docstring, TYVM + "D203", + + # The multi-line docstring summary starts on the same line + "D213", + + # We do not document everything in the docstring + "DOC201", + "DOC402", + "DOC501", + + # The /x regex modifier is common enough in many languages + "FURB167", +] + +[lint.flake8-copyright] +notice-rgx = "(?x) SPDX-FileCopyrightText: \\s \\S" + +[lint.isort] +force-single-line = true +known-first-party = ["pwc_286"] +lines-after-imports = 2 +single-line-exclusions = ["collections.abc", "typing"] + +[lint.per-file-ignores] +# This is a command-line tool; console output is part of its job. +"src/pwc_286/__main__.py" = ["T201"] + +# This is a test suite +"tests/unit/**.py" = ["S101", "T201"] diff --git a/challenge-286/ppentchev/python/scripts/ch-1.py b/challenge-286/ppentchev/python/scripts/ch-1.py new file mode 100755 index 0000000000..a3470d1bd4 --- /dev/null +++ b/challenge-286/ppentchev/python/scripts/ch-1.py @@ -0,0 +1,39 @@ +#!/usr/bin/python3 +# SPDX-FileCopyrightText: Peter Pentchev <roam@ringlet.net> +# SPDX-License-Identifier: BSD-2-Clause +"""Pick a random word from this script's source file and output it.""" + +from __future__ import annotations + +import pathlib +import random +import typing + + +if typing.TYPE_CHECKING: + from typing import Final + + +class EmptyWordError(RuntimeError): + """An empty word was chosen, `str.split()` is not suppoed to do that.""" + + words: list[str] + """The words among which the empty word was found.""" + + def __str__(self) -> str: + """Provide a human-readable error message.""" + return f"Internal error: empty word in {self.words!r}" + + +def main() -> None: + """Find the source file, select a word, output it.""" + words: Final = pathlib.Path(__file__).read_text(encoding="UTF-8").split() + chosen_one: Final = random.choice(words) # noqa: S311 # no cryptography here + if not chosen_one: + raise EmptyWordError(words) + + print(chosen_one) # noqa: T201 # this is the whole point of this program + + +if __name__ == "__main__": + main() diff --git a/challenge-286/ppentchev/python/scripts/ch-2.py b/challenge-286/ppentchev/python/scripts/ch-2.py new file mode 100755 index 0000000000..b8c0819589 --- /dev/null +++ b/challenge-286/ppentchev/python/scripts/ch-2.py @@ -0,0 +1,109 @@ +#!/usr/bin/python3 +# SPDX-FileCopyrightText: Peter Pentchev <roam@ringlet.net> +# SPDX-License-Identifier: BSD-2-Clause +"""Pick a random word from this script's source file and output it.""" + +from __future__ import annotations + +import enum +import os +import sys +import typing + + +if typing.TYPE_CHECKING: + from collections.abc import Callable, Iterable, Iterator + from typing import Final, Self + + +TEST_SEQUENCES: Final = [ + [2, 1, 4, 5, 6, 3, 0, 2], + [0, 5, 3, 2], + [9, 2, 1, 4, 5, 6, 0, 7, 3, 1, 3, 5, 7, 9, 0, 8], +] +"""The sample sequences from the problem.""" + + +class OrderIterState(enum.IntEnum): + """The states that an `OrderIter` object may be in.""" + + STASH = 0 + """Stash a value to pass to `min()` or `max()` later.""" + + YIELD = 1 + """Perform a `min()` or `max()` operation, yield the result.""" + + +class OrderIter: + """Iterate over the numbers of a single round.""" + + ints: Iterator[int] + """The list of integers to process.""" + + state: OrderIterState + """The current state of the iterator.""" + + stashed: int + """The last stashed number.""" + + op: Callable[[int, int], int] + """The operation to perform when yielding a value.""" + + def __init__(self, ints: Iterable[int]) -> None: + """Construct an `OrderIter` object with the specified numbers to process.""" + self.ints = iter(ints) + self.state = OrderIterState.STASH + self.stashed = -616 + self.op = min + + def __iter__(self) -> Self: + """Return the `OrderIter` object itself as an iterator.""" + return self + + def __next__(self) -> int: + """Calculate and return the next number.""" + match self.state: + case OrderIterState.STASH: + self.stashed = next(self.ints) + self.state = OrderIterState.YIELD + return next(self) + + case OrderIterState.YIELD: + res: Final = self.op(self.stashed, next(self.ints)) + self.state = OrderIterState.STASH + self.op = max if self.op == min else min + return res + + +def order_iter(ints: list[int]) -> list[int]: + """Run a single round using an `OrderIter` object.""" + return list(OrderIter(ints)) + + +def run_order_game(ints: list[int]) -> int: + """Run the order game until there is only a single integer left.""" + if not ints: + raise RuntimeError(repr(ints)) + + while len(ints) > 1: + ints = order_iter(ints) + + return ints[0] + + +def parse_stdin() -> list[int]: + """Read a line from the standard input, parse it as a list of integers.""" + return [int(word) for word in sys.stdin.readline().split() if word] + + +def main() -> None: + """Find the source file, select a word, output it.""" + if os.environ.get("PWC_FROM_STDIN", "") == "1": + print(run_order_game(parse_stdin())) # noqa: T201 # this is the whole point + else: + for ints in TEST_SEQUENCES: + print(run_order_game(ints)) # noqa: T201 # this is the whole point + + +if __name__ == "__main__": + main() diff --git a/challenge-286/ppentchev/python/tox.ini b/challenge-286/ppentchev/python/tox.ini new file mode 100644 index 0000000000..5d113b5fd0 --- /dev/null +++ b/challenge-286/ppentchev/python/tox.ini @@ -0,0 +1,71 @@ +# SPDX-FileCopyrightText: Peter Pentchev <roam@ringlet.net> +# SPDX-License-Identifier: BSD-2-Clause + +[tox] +minversion = 4.1 +envlist = + ruff + format + mypy +isolated_build = True + +[defs] +pyfiles = + scripts/ch-1.py \ + scripts/ch-2.py + +[testenv:ruff] +skip_install = True +tags = + check + ruff + quick +deps = + -r requirements/ruff.txt +commands = + ruff check -- {[defs]pyfiles} + +[testenv:format] +skip_install = True +tags = + check + quick +deps = + -r requirements/ruff.txt +commands = + ruff check --config ruff-base.toml --select=D,I --diff -- {[defs]pyfiles} + ruff format --config ruff-base.toml --check --diff -- {[defs]pyfiles} + +[testenv:reformat] +skip_install = True +tags = + format + manual +deps = + -r requirements/ruff.txt +commands = + ruff check --config ruff-base.toml --select=D,I --fix -- {[defs]pyfiles} + ruff format --config ruff-base.toml -- {[defs]pyfiles} + +[testenv:mypy] +skip_install = True +tags = + check +deps = + mypy >= 1, < 2 +setenv = + MYPYPATH = {toxinidir}/stubs +commands = + mypy {[defs]pyfiles} + +[testenv:pyupgrade] +skip_install = True +tags = + check + manual +deps = + pyupgrade >= 3, < 4 +allowlist_externals = + sh +commands = + sh -c 'pyupgrade --py311-plus scripts/*.py' |
